@company-semantics/contracts 39.2.0 → 39.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "39.2.0",
3
+ "version": "39.4.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -108,6 +108,7 @@
108
108
  "generate:spec-hash": "tsx scripts/generate-spec-hash.ts",
109
109
  "generate:spec-hash:check": "tsx scripts/generate-spec-hash.ts --check",
110
110
  "generate:api": "pnpm generate:api-types && pnpm generate:spec-hash && pnpm generate:openapi-routes",
111
+ "generate:api:check": "pnpm generate:api-types:check && pnpm generate:spec-hash:check && pnpm generate:openapi-routes:check",
111
112
  "generate:api-types": "openapi-typescript openapi/backend.yaml -o src/api/generated.ts",
112
113
  "generate:openapi-routes": "tsx scripts/generate-openapi-routes.ts",
113
114
  "generate:openapi-routes:check": "tsx scripts/generate-openapi-routes.ts --check",
@@ -138,13 +139,22 @@
138
139
  "prettier": "^3.9.6",
139
140
  "tsx": "^4.23.1",
140
141
  "typescript": "^5.8.3",
142
+ "vite": "^7.3.6",
141
143
  "vitest": "^4.1.10",
142
144
  "yaml": "^2.9.0"
143
145
  },
144
146
  "pnpm": {
145
147
  "overrides": {
146
- "picomatch": ">=4.0.4",
147
- "brace-expansion@<5.0.8": ">=5.0.8"
148
+ "brace-expansion@<5.0.8": ">=5.0.8",
149
+ "minimatch@<10.2.3": ">=10.2.3",
150
+ "js-yaml@>=4.0.0 <4.3.0": ">=4.3.0 <5.0.0",
151
+ "js-yaml@>=5.0.0 <5.2.2": ">=5.2.2 <6.0.0",
152
+ "rollup@<4.59.0": ">=4.59.0",
153
+ "picomatch@<4.0.4": ">=4.0.4",
154
+ "esbuild@<0.28.1": ">=0.28.1",
155
+ "linkify-it@<5.0.2": ">=5.0.2",
156
+ "postcss@<8.5.18": ">=8.5.18",
157
+ "markdown-it@<14.2.0": ">=14.2.0"
148
158
  }
149
159
  },
150
160
  "lint-staged": {
@@ -152,5 +162,5 @@
152
162
  "*.md": "markdownlint-cli2",
153
163
  "package.json": "node -e \"JSON.parse(require('fs').readFileSync('package.json'))\""
154
164
  },
155
- "securityRequirementsVersion": "8320e7a5612c6877c2959c127ffa78e693b9d1e95805340a5e4af11a01c4a669"
165
+ "securityRequirementsVersion": "888e8f888ed3bee8311fd39dee4b24b4f23a6638f1df183e3e6b4e0455704ae2"
156
166
  }
@@ -0,0 +1,73 @@
1
+ # action-items/
2
+
3
+ ## Purpose
4
+
5
+ The vocabulary for **things awaiting a particular user's decision** — the
6
+ published shape of `GET /api/me/action-items`, which is the one read that
7
+ answers "what needs me?" across every domain at once.
8
+
9
+ Before this, every pending-thing read in the system was scoped to one entity
10
+ (`/docs/{id}/access-requests`) or one org (`/api/workspace/invites`). Nothing
11
+ was keyed by _actor = me, across resources_, so a pending access request on a
12
+ document was invisible from the tree, the sidebar, and the app shell — the only
13
+ real notice was an email.
14
+
15
+ An action item is **derived standing state, never a stored row**. A provider in
16
+ `backend/src/action-items/providers/` answers "what does user U owe action on in
17
+ org O?" from its domain's own pending rows, under its domain's own authority
18
+ gate. Nothing is written, so nothing has to be cleared: when the first owner
19
+ approves a request, the source row leaves `pending` and the item is simply
20
+ absent from the next read. There is no clearing code to forget to write.
21
+
22
+ - `kinds.ts` — `ACTION_ITEM_KINDS` and the `ActionItemKind` union derived from
23
+ it. Also the argument for why this is a different vocabulary from
24
+ `NotificationKind`, and the list of things deliberately excluded.
25
+ - `schemas.ts` — `ActionItem`, `ActionItemTarget`, `ActionItemListResponse`.
26
+ Zod-canonical.
27
+
28
+ ## Invariants
29
+
30
+ - **A kind names STANDING STATE, not an event.** `NotificationKind` names
31
+ things that get SENT (`companyMd.access_requested` — an email left the
32
+ building); an `ActionItemKind` names a decision that is outstanding right now
33
+ (`companyMd.access_request_pending`). They are not in bijection: `auth.otp`
34
+ can never be an action item, `execution.confirmation_pending` has no
35
+ notification, and one item can be owed by many users where the first to act
36
+ closes it for all. Collapsing the unions makes one of those meanings lie.
37
+ - **Copy is not shared with `notifications/`.** An email is prose carrying a
38
+ brand and a year; an action-item row is a title, a detail line and the name of
39
+ the thing. Bending the send-shaped render pipeline to also emit list rows
40
+ would couple derived state to a delivery mechanism. This is a decision, not an
41
+ oversight — see ADR-CONT-104 before "fixing" it.
42
+ - **`href` must RESOLVE the item, not merely show it.** An item you can only
43
+ look at is a notification. This is what keeps the layer honest about being
44
+ actionable-only.
45
+ - **`id` is `${kind}:${sourceRowId}`** — a function of the source row, stable
46
+ across reads. It is not an id of its own, because there is no row of its own.
47
+ - **`unitPath` is a roll-up ANCHOR, not a materialised ancestor path.** It is
48
+ the ltree `path` of the anchor org unit, read live from the same column
49
+ `OrgUnitSchema.path` carries. Clients count a subtree by prefix match
50
+ (`p === path || p.startsWith(`${path}.`)`). Because this and the org tree read
51
+ the same live column, they cannot disagree, and moving a unit needs no
52
+ backfill. Storing an ancestor list here would reintroduce exactly that
53
+ backfill. `null` means org-wide: it rolls up to the root and nowhere below.
54
+ - **Authority is the provider's, never this layer's.** These are shapes; nothing
55
+ here filters. A provider that returns an item the viewer may not act on is a
56
+ provider bug, and its test is the one that must catch it.
57
+ - Types and pure vocabulary only — no classes, no side effects, no imports
58
+ beyond `zod` (vocabulary-guard enforced).
59
+
60
+ ## Public API
61
+
62
+ | Export | Description |
63
+ | --------------------------------------------------- | ------------------------------------------------------------ |
64
+ | `ActionItemKind` / `ACTION_ITEM_KINDS` | The closed vocabulary of outstanding-decision kinds |
65
+ | `ActionItem` / `ActionItemSchema` | One outstanding decision |
66
+ | `ActionItemTarget` / `ActionItemTargetSchema` | What the item is about — the entity a surface badges against |
67
+ | `ActionItemTargetType` / `ACTION_ITEM_TARGET_TYPES` | `company_md` \| `org_unit` \| `org` \| `execution` |
68
+ | `ActionItemListResponse` / `…Schema` | The `GET /api/me/action-items` response |
69
+
70
+ ## Dependencies
71
+
72
+ `zod`, and `./kinds` from `./schemas`. Nothing else — this domain depends on
73
+ nothing outside itself, including `notifications/`.
@@ -0,0 +1,39 @@
1
+ # action-items/\_\_tests\_\_/
2
+
3
+ ## Purpose
4
+
5
+ Locks the claims `../README.md` and ADR-CONT-104 make that the compiler cannot.
6
+
7
+ - `vocabulary.test.ts` — the three things a type signature does not say:
8
+ 1. **`ActionItemKind` and `NotificationKind` share no member.** This is the
9
+ load-bearing test of the whole domain. The two unions look mergeable and
10
+ are not: one names events that get SENT, the other names standing state.
11
+ If someone "unifies" them, this is what stops it.
12
+ 2. **The response carries no field named `version`.** The app's
13
+ `invalidateResource` probes cached payloads for that name structurally and
14
+ SKIPS the invalidation when the cached value is at least as new. A field of
15
+ that name here would silently wedge every badge in the product, with no
16
+ error anywhere. Nothing else in the stack would catch it.
17
+ 3. **A null `unitPath` parses.** An execution has no owning org unit, so it
18
+ must be able to say so rather than carry a fabricated anchor.
19
+
20
+ ## Invariants
21
+
22
+ - These tests assert VOCABULARY, never behaviour — there is no behaviour here to
23
+ assert. A test that needs a provider, a fetch, or a database belongs in
24
+ backend's `src/action-items/__tests__/`, not here.
25
+ - The disjointness test reads `NOTIFICATION_DEFINITIONS` rather than restating
26
+ the notification kinds. A hand-copied list would drift and start passing
27
+ vacuously.
28
+ - `makeItem()` returns a well-formed item and every negative case mutates ONE
29
+ field of it. A negative test that hand-builds a broken object can pass for the
30
+ wrong reason.
31
+
32
+ ## Public API
33
+
34
+ None — test-only.
35
+
36
+ ## Dependencies
37
+
38
+ `vitest`, the sibling modules under test, `../../notifications` (for the real
39
+ kind list), and `../../resource-keys` (for the key round-trip).
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The action-item vocabulary's invariants, as tests rather than prose
3
+ * (ADR-CONT-104).
4
+ *
5
+ * These lock the two claims the README makes that a compiler cannot: that this
6
+ * is a SEPARATE vocabulary from NotificationKind, and that an action item is
7
+ * addressable both as a cache key and as a roll-up anchor.
8
+ */
9
+ import { describe, it, expect } from "vitest";
10
+ import { ACTION_ITEM_KINDS, type ActionItemKind } from "../kinds.js";
11
+ import {
12
+ ACTION_ITEM_TARGET_TYPES,
13
+ ActionItemListResponseSchema,
14
+ ActionItemSchema,
15
+ } from "../schemas.js";
16
+ import { NOTIFICATION_DEFINITIONS } from "../../notifications/index.js";
17
+ import {
18
+ toQueryKey,
19
+ fromQueryKey,
20
+ type ResourceKey,
21
+ } from "../../resource-keys.js";
22
+
23
+ const ORG_ID = "11111111-1111-4111-8111-111111111111";
24
+
25
+ function makeItem(over: Partial<Record<string, unknown>> = {}) {
26
+ return {
27
+ id: "companyMd.access_request_pending:req-1",
28
+ kind: "companyMd.access_request_pending" satisfies ActionItemKind,
29
+ target: { type: "company_md", id: "doc-1" },
30
+ unitPath: "acme.sales",
31
+ title: "Maya Chen wants access",
32
+ detail: "Reviewing the cross-team roadmap",
33
+ contextLabel: "Sales.md",
34
+ href: "/@acme/md/doc-1?share=1&request=req-1",
35
+ createdAt: "2026-07-27T00:00:00.000Z",
36
+ ...over,
37
+ };
38
+ }
39
+
40
+ describe("ActionItemKind is a separate vocabulary from NotificationKind", () => {
41
+ // The core claim of ADR-CONT-104. If someone "unifies" the two unions,
42
+ // this is the test that should stop them: the overlap must stay empty, because
43
+ // the two name different things (a sent event vs. standing state).
44
+ it("shares no member with NotificationKind", () => {
45
+ const notificationKinds = new Set(Object.keys(NOTIFICATION_DEFINITIONS));
46
+ const overlap = ACTION_ITEM_KINDS.filter((k) => notificationKinds.has(k));
47
+ expect(overlap).toEqual([]);
48
+ });
49
+
50
+ it("names standing state — every kind reads as a pending condition", () => {
51
+ for (const kind of ACTION_ITEM_KINDS) {
52
+ expect(kind).toMatch(/_pending$/);
53
+ }
54
+ });
55
+
56
+ it("uses the shared {domain}.{type} convention", () => {
57
+ for (const kind of ACTION_ITEM_KINDS) {
58
+ expect(kind).toMatch(/^[a-zA-Z]+\.[a-z_]+$/);
59
+ }
60
+ });
61
+ });
62
+
63
+ describe("ActionItemSchema", () => {
64
+ it("accepts a well-formed item", () => {
65
+ expect(ActionItemSchema.parse(makeItem())).toMatchObject({
66
+ kind: "companyMd.access_request_pending",
67
+ });
68
+ });
69
+
70
+ it("accepts a null unitPath — an org-wide item with no tree anchor", () => {
71
+ // Proves the shape admits targets that live outside the org tree at all
72
+ // (an execution has no owning unit). Those roll up to the root and nowhere
73
+ // below it; a required unitPath would have forced a fake anchor.
74
+ const parsed = ActionItemSchema.parse(
75
+ makeItem({
76
+ kind: "execution.confirmation_pending",
77
+ target: { type: "execution", id: "exec-1" },
78
+ unitPath: null,
79
+ }),
80
+ );
81
+ expect(parsed.unitPath).toBeNull();
82
+ });
83
+
84
+ it("rejects a kind outside the vocabulary", () => {
85
+ expect(() =>
86
+ ActionItemSchema.parse(makeItem({ kind: "org.invite" })),
87
+ ).toThrow();
88
+ });
89
+
90
+ it("rejects a target type outside the vocabulary", () => {
91
+ expect(() =>
92
+ ActionItemSchema.parse(makeItem({ target: { type: "chat", id: "c-1" } })),
93
+ ).toThrow();
94
+ });
95
+
96
+ it("requires an href — an item you cannot resolve is not an action item", () => {
97
+ const { href: _dropped, ...withoutHref } = makeItem();
98
+ expect(() => ActionItemSchema.parse(withoutHref)).toThrow();
99
+ });
100
+
101
+ it("carries no field named `version`", () => {
102
+ // invalidateResource() probes a cached payload for `version` structurally
103
+ // and SKIPS the invalidation when the cached value is at least as new. A
104
+ // field of that name in this response would silently wedge every badge.
105
+ expect(Object.keys(ActionItemSchema.shape)).not.toContain("version");
106
+ expect(Object.keys(ActionItemListResponseSchema.shape)).not.toContain(
107
+ "version",
108
+ );
109
+ });
110
+ });
111
+
112
+ describe("the actionItems resource key", () => {
113
+ const key: ResourceKey = { type: "actionItems", orgId: ORG_ID };
114
+
115
+ it("is org-scoped, so switching org cannot serve the previous org's bucket", () => {
116
+ expect(toQueryKey(key)).toEqual(["actionItems", ORG_ID]);
117
+ });
118
+
119
+ it("roundtrips through fromQueryKey", () => {
120
+ expect(fromQueryKey(toQueryKey(key))).toEqual(key);
121
+ });
122
+ });
123
+
124
+ describe("target types", () => {
125
+ it("covers every surface a badge can hang on", () => {
126
+ expect([...ACTION_ITEM_TARGET_TYPES].sort()).toEqual([
127
+ "company_md",
128
+ "execution",
129
+ "org",
130
+ "org_unit",
131
+ ]);
132
+ });
133
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * action-items/ — the vocabulary for things awaiting a user's decision.
3
+ *
4
+ * See ./README.md for the domain, and ADR-CONT-104 for why this is not
5
+ * `NotificationKind`.
6
+ */
7
+
8
+ // =============================================================================
9
+ // Kinds — what sorts of decision can be outstanding
10
+ // =============================================================================
11
+
12
+ export { ACTION_ITEM_KINDS } from "./kinds";
13
+ export type { ActionItemKind } from "./kinds";
14
+
15
+ // =============================================================================
16
+ // Shapes — the published GET /api/me/action-items contract
17
+ // =============================================================================
18
+
19
+ export {
20
+ ACTION_ITEM_TARGET_TYPES,
21
+ ActionItemKindSchema,
22
+ ActionItemListResponseSchema,
23
+ ActionItemSchema,
24
+ ActionItemTargetSchema,
25
+ ActionItemTargetTypeSchema,
26
+ } from "./schemas";
27
+
28
+ export type {
29
+ ActionItem,
30
+ ActionItemListResponse,
31
+ ActionItemTarget,
32
+ ActionItemTargetType,
33
+ } from "./schemas";
@@ -0,0 +1,82 @@
1
+ /**
2
+ * What kinds of action item exist (ADR-CONT-104).
3
+ *
4
+ * An action item is a thing AWAITING A PARTICULAR USER'S DECISION. It is
5
+ * derived standing state, never a stored row: a provider answers "what does
6
+ * user U owe action on in org O?" from its domain's own pending rows, so an
7
+ * item resolved by someone else is simply absent from the next read.
8
+ *
9
+ * WHY THIS IS NOT `NotificationKind`:
10
+ *
11
+ * `NotificationKind` names EVENTS THAT GET SENT — `companyMd.access_requested`
12
+ * is the moment an email leaves the building. An action item names STANDING
13
+ * STATE — `companyMd.access_request_pending` is the fact that a decision is
14
+ * outstanding right now. The two are not in bijection and never will be:
15
+ *
16
+ * - `auth.otp` is a notification and can never be an action item.
17
+ * - `execution.confirmation_pending` is an action item with no notification.
18
+ * - One notification kind can open an item that many DIFFERENT users owe, and
19
+ * the first of them to act closes it for all of them. A sent notification has
20
+ * no such lifecycle — it is delivered once, to one address, and is then over.
21
+ *
22
+ * Collapsing the two unions would make one of those two meanings lie. Keep them
23
+ * separate; if a future kind needs both, it gets a member in each.
24
+ *
25
+ * Copy is likewise NOT shared with `NotificationDefinition.compose`. An email is
26
+ * prose carrying a brand and a year; an action-item row is a title, a detail
27
+ * line and the name of the thing. Bending the send-shaped render pipeline to
28
+ * also emit list rows would couple derived state to a delivery mechanism.
29
+ *
30
+ * INVARIANTS:
31
+ * - `{domain}.{type}` dot notation, matching `NotificationKind`'s convention.
32
+ * The `_pending` suffix is load-bearing: it is what makes the "standing state,
33
+ * not event" reading obvious at the call site.
34
+ * - These strings go on the wire and are used as the stable prefix of an item's
35
+ * id. Renaming one is a breaking change, not a tidy-up.
36
+ * - A kind earns membership by having a PROVIDER that can derive it and an
37
+ * `href` that RESOLVES it. A kind whose item cannot be acted on is not an
38
+ * action item — that is the informational feed, which this layer does not
39
+ * model (yet).
40
+ */
41
+
42
+ // =============================================================================
43
+ // ActionItemKind Union
44
+ // =============================================================================
45
+
46
+ /**
47
+ * ActionItemKind identifies what sort of decision is outstanding.
48
+ *
49
+ * New kinds MUST be added to:
50
+ * 1. This union
51
+ * 2. A provider in `backend/src/action-items/providers/`, which applies that
52
+ * domain's OWN authority gate
53
+ *
54
+ * `execution.confirmation_pending` earns its place by fixing a real hole rather
55
+ * than by symmetry: a chat execution in `pending_confirmation` is reachable
56
+ * ONLY from the chat stream it was proposed in, so closing the tab strands it
57
+ * until it expires. Its confirm/reject routes already exist
58
+ * (`POST /api/executions/{id}/confirm|reject`); what was missing was any way to
59
+ * find it. That is exactly what this layer is for.
60
+ *
61
+ * The candidates that did NOT earn a place, recorded so they are not
62
+ * re-litigated — every one fails a membership test above:
63
+ *
64
+ * - **A pending INVITE awaits the invitee**, who is not yet in the org. No
65
+ * viewer owes it a decision. The `+ N invite` counts in the people surfaces
66
+ * are VOLUME badges and stay where they are.
67
+ * - **An ownership TRANSFER is the recipient's decision** — but accepting is
68
+ * token-bearing (`token + session + email match + fresh login`) and only the
69
+ * token's hash is stored, so an in-app row could not resolve it. "Go check
70
+ * your email" is a dead affordance.
71
+ * - **`execution_approvals`** (the approval-policy chain) has no `org_id` of
72
+ * its own, names a ROLE rather than a user as approver, and has zero HTTP
73
+ * surface. Distinct from `execution.confirmation_pending` above.
74
+ * - **`orgReasoning.inference_review`** is semantically a clean fit and its
75
+ * decision route exists — with no caller and no review UI to link to.
76
+ */
77
+ export const ACTION_ITEM_KINDS = [
78
+ "companyMd.access_request_pending",
79
+ "execution.confirmation_pending",
80
+ ] as const;
81
+
82
+ export type ActionItemKind = (typeof ACTION_ITEM_KINDS)[number];
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Action-item DTOs (ADR-CONT-104).
3
+ *
4
+ * The published shape of `GET /api/me/action-items` — everything awaiting the
5
+ * viewer's decision across every domain, in one read.
6
+ *
7
+ * Zod-canonical: schema is the source of truth, the type is inferred.
8
+ */
9
+ import { z } from "zod";
10
+ import { ACTION_ITEM_KINDS } from "./kinds";
11
+
12
+ /**
13
+ * What an item is ABOUT — the entity a surface badges against.
14
+ *
15
+ * A small union of its own rather than the app-local `SHAREABLE_ENTITY_TYPES`
16
+ * (which contracts does not publish) or `ResourceKey` (a cache vocabulary whose
17
+ * company.md variant carries the doc id under a field named `slug`). Promoting
18
+ * either is its own change; this stays sized to what badges exist.
19
+ */
20
+ export const ACTION_ITEM_TARGET_TYPES = [
21
+ "company_md",
22
+ "org_unit",
23
+ "org",
24
+ // Not an ACL entity — an execution is addressed by id and has no owning unit,
25
+ // so its items anchor at `unitPath: null` and roll up only to the root. That
26
+ // asymmetry is deliberate and is what proves the roll-up handles targets with
27
+ // no place in the org tree.
28
+ "execution",
29
+ ] as const;
30
+ export const ActionItemTargetTypeSchema = z.enum(ACTION_ITEM_TARGET_TYPES);
31
+ export type ActionItemTargetType = z.infer<typeof ActionItemTargetTypeSchema>;
32
+
33
+ export const ActionItemTargetSchema = z.object({
34
+ type: ActionItemTargetTypeSchema,
35
+ /** The entity's stable id. Docs are id-addressed (ADR-BE-315), never slug. */
36
+ id: z.string(),
37
+ });
38
+ export type ActionItemTarget = z.infer<typeof ActionItemTargetSchema>;
39
+
40
+ /** Zod mirror of the vocabulary. `./kinds` owns the list; this never restates it. */
41
+ export const ActionItemKindSchema = z.enum(ACTION_ITEM_KINDS);
42
+
43
+ /**
44
+ * One outstanding decision.
45
+ *
46
+ * Derived, never stored — see `./kinds`. Two consequences worth stating at the
47
+ * shape rather than in prose elsewhere:
48
+ *
49
+ * - `id` is `${kind}:${sourceRowId}`, stable across reads because it is a
50
+ * function of the source row. It is NOT a row id of its own; there is no row.
51
+ * - `unitPath` is the ltree `path` of the item's anchor org unit, read live from
52
+ * the same column `OrgUnitSchema.path` carries. It is a ROLL-UP ANCHOR, not a
53
+ * materialised ancestor list: the client counts a subtree by prefix-matching
54
+ * (`p === path || p.startsWith(`${path}.`)`), and because both this and the
55
+ * org tree are read from the same live column, they cannot disagree. Moving a
56
+ * unit needs no backfill. `null` means org-wide — it rolls up to the root and
57
+ * nowhere below it.
58
+ */
59
+ export const ActionItemSchema = z.object({
60
+ /** `${kind}:${sourceRowId}` — a function of the source row, not a row id. */
61
+ id: z.string(),
62
+ kind: ActionItemKindSchema,
63
+ target: ActionItemTargetSchema,
64
+ /** ltree anchor for subtree roll-up; `null` = org-wide. */
65
+ unitPath: z.string().nullable(),
66
+ /** One line naming the decision, e.g. "Maya Chen wants access". */
67
+ title: z.string(),
68
+ /** Optional second line — the requester's message, a reason, a deadline. */
69
+ detail: z.string().nullable(),
70
+ /** The thing it is about, as the user names it, e.g. "Sales.md". */
71
+ contextLabel: z.string(),
72
+ /**
73
+ * Where to go to RESOLVE it. An action item whose href only *shows* the thing
74
+ * is a notification, not an action item — see `./kinds`.
75
+ */
76
+ href: z.string(),
77
+ createdAt: z.string(),
78
+ });
79
+ export type ActionItem = z.infer<typeof ActionItemSchema>;
80
+
81
+ /** `GET /api/me/action-items` — everything awaiting this viewer, newest first. */
82
+ export const ActionItemListResponseSchema = z.object({
83
+ items: z.array(ActionItemSchema),
84
+ });
85
+ export type ActionItemListResponse = z.infer<
86
+ typeof ActionItemListResponseSchema
87
+ >;
package/src/api/README.md CHANGED
@@ -12,7 +12,7 @@ Provides reusable functions that encode API response shapes shared between the c
12
12
  - Response shapes must match the types defined in `src/mcp/` (e.g., `ToolDiscoveryResponse`)
13
13
  - Capability graph inclusion is opt-in via query parameter convention (`?include=graph`)
14
14
  - `generated.ts` contains only type exports — zero runtime imports, zero const/let/var/function declarations
15
- - `generated.ts` must stay in sync with `openapi/backend.yaml` — run `pnpm generate:api-types:check` to verify
15
+ - `generated.ts` must stay in sync with `openapi/backend.yaml` — run `pnpm generate:api:check` to verify (it checks all three spec-derived artifacts, not just this one)
16
16
 
17
17
  ## Generated Types (`generated.ts`)
18
18
 
@@ -22,7 +22,7 @@ Auto-generated by `openapi-typescript` from `openapi/backend.yaml`. Exports thre
22
22
  - `components` — All schema types (MeResponse, WorkspaceOverview, etc.)
23
23
  - `operations` — Operation-level types keyed by operationId
24
24
 
25
- Regenerate with `pnpm generate:api-types`. Check for staleness with `pnpm generate:api-types:check`.
25
+ Regenerate with `pnpm generate:api` — never `pnpm generate:api-types` alone. Three artifacts derive from `openapi/backend.yaml` (this file, `generated-spec-hash.ts`, and `src/generated/openapi-routes.ts`); regenerating one leaves the others stale. Check for staleness with `pnpm generate:api:check`.
26
26
 
27
27
  The file is committed to the repo (not .gitignored) so consumers can import types without running the generator, and type changes are visible in code review diffs.
28
28
 
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
2
- export const SPEC_HASH = '4669379f3c9c' as const;
3
- export const SPEC_HASH_FULL = '4669379f3c9c9733dea3f5bf53f2c1c0d1f0e496fddbf874e8d1f1951e805d7c' as const;
2
+ export const SPEC_HASH = '578211b8b148' as const;
3
+ export const SPEC_HASH_FULL = '578211b8b1489b1d26938629c14b09a94ee3bf636085a5f058f1b573747f8fce' as const;