@company-semantics/contracts 92.0.0 → 93.1.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 +13 -12
- package/src/__tests__/resource-keys.test.ts +1 -0
- package/src/action-items/README.md +12 -4
- package/src/action-items/schemas.ts +10 -0
- package/src/affordances/README.md +52 -1
- package/src/affordances/__tests__/README.md +10 -1
- package/src/affordances/__tests__/affordances.test.ts +13 -1
- package/src/affordances/__tests__/catalog-wave.test.ts +237 -0
- package/src/affordances/affordance.ts +56 -4
- package/src/affordances/args.ts +44 -1
- package/src/affordances/available.ts +59 -0
- package/src/affordances/coverage.ts +51 -0
- package/src/affordances/ids.ts +28 -0
- package/src/affordances/index.ts +17 -0
- package/src/api/README.md +1 -0
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +506 -1
- package/src/api/primitives.ts +13 -0
- package/src/billing/README.md +1 -0
- package/src/billing/refusals.ts +12 -0
- package/src/chat/__tests__/page-context.test.ts +22 -43
- package/src/chat/__tests__/runtime-profile.test.ts +20 -44
- package/src/generated/openapi-routes.ts +1 -0
- package/src/message-parts/README.md +5 -0
- package/src/message-parts/affordance-offer.ts +44 -0
- package/src/message-parts/index.ts +7 -0
- package/src/message-parts/types.ts +7 -1
- package/src/message-parts/wire.ts +18 -0
- package/src/notifications/README.md +17 -3
- package/src/notifications/__tests__/README.md +4 -0
- package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +27 -25
- package/src/notifications/__tests__/fixtures.ts +7 -1
- package/src/notifications/__tests__/grounded-ctas.test.ts +103 -0
- package/src/notifications/__tests__/render-snapshot.test.ts +1 -1
- package/src/notifications/content.ts +9 -0
- package/src/notifications/kinds/billing-auto-recharge-failed.ts +4 -2
- package/src/notifications/kinds/billing-payment-past-due.ts +2 -0
- package/src/notifications/kinds/billing-suspended.ts +2 -0
- package/src/notifications/kinds/chat-shared.ts +10 -3
- package/src/notifications/kinds/company-md-access-approved.ts +13 -6
- package/src/notifications/kinds/share-granted.ts +1 -1
- package/src/notifications/kinds/unit-owner-granted.ts +9 -2
- package/src/notifications/payloads.ts +6 -0
- package/src/proactive/README.md +18 -2
- package/src/proactive/__tests__/README.md +11 -1
- package/src/proactive/__tests__/chat-templates.test.ts +7 -6
- package/src/proactive/__tests__/proactive-offers.test.ts +124 -0
- package/src/proactive/__tests__/vocabulary.test.ts +1 -1
- package/src/proactive/composer.ts +11 -0
- package/src/proactive/templates/README.md +11 -3
- package/src/proactive/templates/billing-auto-recharge-failed.ts +1 -0
- package/src/proactive/templates/billing-balance-exhausted.ts +2 -6
- package/src/proactive/templates/billing-payment-past-due.ts +1 -0
- package/src/proactive/templates/billing-subscription-canceled.ts +1 -5
- package/src/proactive/templates/billing-suspended.ts +1 -0
- package/src/proactive/templates/morning-brief.ts +1 -0
- package/src/proactive/templates/org-became-shared.ts +4 -3
- package/src/proactive/templates/source-sync-review-ready.ts +3 -8
- package/src/proactive/templates/workflow-needs-attention.ts +1 -0
- package/src/rate-limit/README.md +27 -1
- package/src/rate-limit/__tests__/README.md +23 -0
- package/src/rate-limit/__tests__/rate-limited-error.test.ts +53 -0
- package/src/rate-limit/index.ts +33 -0
- package/src/resource-key-tables.ts +1 -0
- package/src/resource-key-types.ts +3 -0
- package/src/resource-keys.ts +1 -0
- package/src/root-exports/agent-runtime.ts +14 -2
- package/src/root-exports/delivery.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@company-semantics/contracts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "93.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -142,33 +142,33 @@
|
|
|
142
142
|
"node": "22.x"
|
|
143
143
|
},
|
|
144
144
|
"dependencies": {
|
|
145
|
-
"@slack/types": "^3.
|
|
145
|
+
"@slack/types": "^3.2.0",
|
|
146
146
|
"zod": "^4.6.5"
|
|
147
147
|
},
|
|
148
148
|
"devDependencies": {
|
|
149
149
|
"@eslint/js": "^10.0.1",
|
|
150
|
-
"@types/node": "^22.20.
|
|
150
|
+
"@types/node": "^22.20.5",
|
|
151
151
|
"amphtml-validator": "^1.0.38",
|
|
152
152
|
"culori": "^4.0.2",
|
|
153
|
-
"dependency-cruiser": "18.
|
|
154
|
-
"eslint": "^10.
|
|
153
|
+
"dependency-cruiser": "18.5.0",
|
|
154
|
+
"eslint": "^10.12.0",
|
|
155
155
|
"husky": "^9.1.7",
|
|
156
|
-
"knip": "6.
|
|
156
|
+
"knip": "6.39.0",
|
|
157
157
|
"lint-staged": "^17.6.0",
|
|
158
158
|
"markdownlint-cli2": "^0.23.3",
|
|
159
159
|
"openapi-typescript": "^7.13.0",
|
|
160
160
|
"prettier": "^3.9.9",
|
|
161
161
|
"tsx": "^4.23.15",
|
|
162
162
|
"typescript": "^5.8.3",
|
|
163
|
-
"typescript-eslint": "^8.
|
|
164
|
-
"vite": "^8.3.
|
|
165
|
-
"vitest": "^5.0.
|
|
163
|
+
"typescript-eslint": "^8.71.0",
|
|
164
|
+
"vite": "^8.3.2",
|
|
165
|
+
"vitest": "^5.0.3",
|
|
166
166
|
"yaml": "^2.9.1"
|
|
167
167
|
},
|
|
168
168
|
"pnpm": {
|
|
169
169
|
"overrides": {
|
|
170
170
|
"brace-expansion@<5.0.11": ">=5.0.11",
|
|
171
|
-
"smol-toml@<1.
|
|
171
|
+
"smol-toml@<1.9.0": ">=1.9.0",
|
|
172
172
|
"minimatch@<10.2.3": ">=10.2.3",
|
|
173
173
|
"nanoid@<3.3.18": ">=3.3.18",
|
|
174
174
|
"js-yaml@>=4.0.0 <4.3.2": ">=4.3.2 <5.0.0",
|
|
@@ -181,7 +181,8 @@
|
|
|
181
181
|
"postcss@<8.5.23": ">=8.5.23",
|
|
182
182
|
"source-map-js@<1.2.2": ">=1.2.2",
|
|
183
183
|
"markdown-it@<14.2.0": ">=14.2.0",
|
|
184
|
-
"@humanfs/node@<0.16.8": ">=0.16.8 <0.17.0"
|
|
184
|
+
"@humanfs/node@<0.16.8": ">=0.16.8 <0.17.0",
|
|
185
|
+
"katex@<0.18.2": ">=0.18.2 <0.19.0"
|
|
185
186
|
}
|
|
186
187
|
},
|
|
187
188
|
"lint-staged": {
|
|
@@ -189,5 +190,5 @@
|
|
|
189
190
|
"*.md": "markdownlint-cli2",
|
|
190
191
|
"package.json": "node -e \"JSON.parse(require('fs').readFileSync('package.json'))\""
|
|
191
192
|
},
|
|
192
|
-
"securityRequirementsVersion": "
|
|
193
|
+
"securityRequirementsVersion": "71a26f10642b9fc598f6da99c75200fd174ebd0e6a9e32437a3f1b245e906792"
|
|
193
194
|
}
|
|
@@ -42,6 +42,13 @@ absent from the next read. There is no clearing code to forget to write.
|
|
|
42
42
|
- **`href` must RESOLVE the item, not merely show it.** An item you can only
|
|
43
43
|
look at is a notification. This is what keeps the layer honest about being
|
|
44
44
|
actionable-only.
|
|
45
|
+
- **`affordance` is optional; `href` is the fallback.** When a catalog
|
|
46
|
+
affordance resolves the item (control ADR `affordance-system`), the provider
|
|
47
|
+
sets it and a surface that can render a real control renders that —
|
|
48
|
+
re-gated per viewer, executed by the server. When it is absent, the surface
|
|
49
|
+
links to `href`. Any instance is allowed, including the projected
|
|
50
|
+
`review.open`: an item is derived on read and never stored, so nothing is
|
|
51
|
+
stored twice.
|
|
45
52
|
- **`id` is `${kind}:${sourceRowId}`** — a function of the source row, stable
|
|
46
53
|
across reads. It is not an id of its own, because there is no row of its own.
|
|
47
54
|
- **`unitPath` is a roll-up ANCHOR, not a materialized ancestor path.** It is
|
|
@@ -54,8 +61,9 @@ absent from the next read. There is no clearing code to forget to write.
|
|
|
54
61
|
- **Authority is the provider's, never this layer's.** These are shapes; nothing
|
|
55
62
|
here filters. A provider that returns an item the viewer may not act on is a
|
|
56
63
|
provider bug, and its test is the one that must catch it.
|
|
57
|
-
- Types and pure vocabulary only — no classes, no side effects, no
|
|
58
|
-
beyond `zod` (vocabulary-guard
|
|
64
|
+
- Types and pure vocabulary only — no classes, no side effects, no runtime
|
|
65
|
+
imports beyond `zod` and sibling contracts modules (vocabulary-guard
|
|
66
|
+
enforced).
|
|
59
67
|
|
|
60
68
|
## Kinds
|
|
61
69
|
|
|
@@ -80,5 +88,5 @@ absent from the next read. There is no clearing code to forget to write.
|
|
|
80
88
|
|
|
81
89
|
## Dependencies
|
|
82
90
|
|
|
83
|
-
`zod`,
|
|
84
|
-
|
|
91
|
+
`zod`, `./kinds` from `./schemas`, and `../affordances/affordance` for the
|
|
92
|
+
optional `affordance`. Nothing else — in particular not `notifications/`.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Zod-canonical: schema is the source of truth, the type is inferred.
|
|
8
8
|
*/
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
import { AffordanceSchema } from "../affordances/affordance";
|
|
10
11
|
import { ACTION_ITEM_KINDS } from "./kinds";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -91,6 +92,15 @@ export const ActionItemSchema = z.object({
|
|
|
91
92
|
* an action item — see `./kinds`.
|
|
92
93
|
*/
|
|
93
94
|
href: z.string(),
|
|
95
|
+
/**
|
|
96
|
+
* The catalog affordance that resolves it, when one does (control ADR
|
|
97
|
+
* affordance-system). A surface that can render a real control renders this
|
|
98
|
+
* — re-gated per viewer, executed by the server — and falls back to `href`
|
|
99
|
+
* when it is absent. Any instance, including a projected one (`review.open`):
|
|
100
|
+
* an action item is derived on read and never stored, so nothing is stored
|
|
101
|
+
* twice.
|
|
102
|
+
*/
|
|
103
|
+
affordance: AffordanceSchema.optional(),
|
|
94
104
|
createdAt: z.string(),
|
|
95
105
|
});
|
|
96
106
|
export type ActionItem = z.infer<typeof ActionItemSchema>;
|
|
@@ -43,12 +43,59 @@ only the binding KIND.
|
|
|
43
43
|
whether an action executes.
|
|
44
44
|
- **`MAX_OFFERED_AFFORDANCES` is a sanity bound**, never a presentation cap.
|
|
45
45
|
How many a surface shows is that surface's decision.
|
|
46
|
+
- **The burn-down wave is appended, never inserted** (PRD-01137). Twelve ids
|
|
47
|
+
follow `review.open`: `integrations.open`, `hris.connect`,
|
|
48
|
+
`account.editProfile`, `workspace.openOrgSettings`, `sourceSync.addFile`,
|
|
49
|
+
`sourceSync.openUpdate`, `orgStructure.openReview`, `chat.open`,
|
|
50
|
+
`companyMd.openDoc`, `companyMd.openHistory`, `team.open`, `execution.open`
|
|
51
|
+
(arguments and binding kinds in [the burn-down wave](#the-burn-down-wave)
|
|
52
|
+
below). An action on one thing names it by its id, never by a URL — the
|
|
53
|
+
binding owns the route. None reads a live availability condition yet; adding
|
|
54
|
+
one is an append.
|
|
55
|
+
- **Reverse coverage is exactly one of two things.** Every capability a person
|
|
56
|
+
can act on declares `{ affordance }` or `{ noAffordance }` with a reason from
|
|
57
|
+
the closed `NO_AFFORDANCE_REASONS` (`readOnly`, `inlineControl`,
|
|
58
|
+
`internalAdmin`, `systemOnly`, `authFlow`), so "nobody decided" is not
|
|
59
|
+
representable.
|
|
60
|
+
- **"What can I do here" is a result shape, not a catalog listing.**
|
|
61
|
+
`AvailableAffordancesSchema` holds the policy-allowed instances the context's
|
|
62
|
+
offer rules produced, each with its live availability — bounded by
|
|
63
|
+
`MAX_OFFERED_AFFORDANCES`, no instance twice, and no policy field, so a
|
|
64
|
+
blocked one cannot leak. Nothing here enumerates the catalog.
|
|
65
|
+
|
|
66
|
+
## The burn-down wave
|
|
67
|
+
|
|
68
|
+
The twelve ids appended after `review.open` (PRD-01137), in catalog order.
|
|
69
|
+
Arguments come from `AFFORDANCE_ARGS` / `NO_ARG_AFFORDANCE_IDS` (`args.ts`); an
|
|
70
|
+
id with no arguments has no `args` key. The binding kind comes from
|
|
71
|
+
`AFFORDANCE_BINDING_KIND_OF` (`affordance.ts`); none of the twelve is a
|
|
72
|
+
`command`. All twelve read no live availability condition (`null` in
|
|
73
|
+
`AFFORDANCE_AVAILABILITY_OF`).
|
|
74
|
+
|
|
75
|
+
| Id | Args (schema) | Binding kind |
|
|
76
|
+
| --------------------------- | ----------------------------------------------------- | ------------ |
|
|
77
|
+
| `integrations.open` | none | `navigate` |
|
|
78
|
+
| `hris.connect` | none | `surface` |
|
|
79
|
+
| `account.editProfile` | none | `navigate` |
|
|
80
|
+
| `workspace.openOrgSettings` | none | `navigate` |
|
|
81
|
+
| `sourceSync.addFile` | none | `surface` |
|
|
82
|
+
| `sourceSync.openUpdate` | `{ proposalId }` (`SourceSyncOpenUpdateArgsSchema`) | `surface` |
|
|
83
|
+
| `orgStructure.openReview` | `{ proposalId }` (`OrgStructureOpenReviewArgsSchema`) | `surface` |
|
|
84
|
+
| `chat.open` | `{ chatId }` (`ChatOpenArgsSchema`) | `navigate` |
|
|
85
|
+
| `companyMd.openDoc` | `{ docId }` (`CompanyMdDocArgsSchema`) | `navigate` |
|
|
86
|
+
| `companyMd.openHistory` | `{ docId }` (`CompanyMdDocArgsSchema`) | `navigate` |
|
|
87
|
+
| `team.open` | `{ unitId }` (`TeamOpenArgsSchema`) | `navigate` |
|
|
88
|
+
| `execution.open` | `{ executionId }` (`ExecutionOpenArgsSchema`) | `navigate` |
|
|
89
|
+
|
|
90
|
+
Every argument is a non-empty string and every args object is strict.
|
|
46
91
|
|
|
47
92
|
## Public API
|
|
48
93
|
|
|
49
94
|
- `AFFORDANCE_IDS`, `AffordanceId`, `AffordanceIdSchema` (`ids.ts`).
|
|
50
95
|
- `AFFORDANCE_ARGS`, `NO_ARG_AFFORDANCE_IDS`, `ReviewOpenArgsSchema`,
|
|
51
|
-
`
|
|
96
|
+
`SourceSyncOpenUpdateArgsSchema`, `OrgStructureOpenReviewArgsSchema`,
|
|
97
|
+
`ChatOpenArgsSchema`, `CompanyMdDocArgsSchema`, `TeamOpenArgsSchema`,
|
|
98
|
+
`ExecutionOpenArgsSchema`, `AffordanceArgs` (`args.ts`).
|
|
52
99
|
- `AFFORDANCE_BINDING_KINDS`, `AFFORDANCE_AVAILABILITY_IDS`,
|
|
53
100
|
`AffordanceAvailabilityIdSchema`, `AFFORDANCE_UNAVAILABLE_REASONS`,
|
|
54
101
|
`AffordanceAvailabilitySchema` and their types (`availability.ts`).
|
|
@@ -56,6 +103,10 @@ only the binding KIND.
|
|
|
56
103
|
`AffordanceSchema`, `OfferedAffordanceSchema`, `PROJECTED_AFFORDANCE_IDS`,
|
|
57
104
|
`AFFORDANCE_BINDING_KIND_OF`, `AFFORDANCE_AVAILABILITY_OF`,
|
|
58
105
|
`MAX_OFFERED_AFFORDANCES`, `sameAffordances` (`affordance.ts`).
|
|
106
|
+
- `NO_AFFORDANCE_REASONS`, `NoAffordanceReason`, `NoAffordanceReasonSchema`,
|
|
107
|
+
`AffordanceCoverage`, `AffordanceCoverageSchema` (`coverage.ts`).
|
|
108
|
+
- `AvailableAffordance`, `AvailableAffordanceSchema`, `AvailableAffordances`,
|
|
109
|
+
`AvailableAffordancesSchema` (`available.ts`).
|
|
59
110
|
|
|
60
111
|
Published from the package root through `src/root-exports/delivery.ts`.
|
|
61
112
|
|
|
@@ -13,6 +13,14 @@ Locks the affordance vocabulary and the shape of an affordance instance.
|
|
|
13
13
|
canonical args, every schema converts to JSON Schema the way the backend's
|
|
14
14
|
OpenAPI generation converts it, and every name is reachable from the package
|
|
15
15
|
root as the same binding.
|
|
16
|
+
- `catalog-wave.test.ts` — pins the twelve burn-down ids (PRD-01137) appended
|
|
17
|
+
after the original seven, their binding kinds and null availability; proves a
|
|
18
|
+
targeted id requires exactly its one non-empty id arg, the no-affordance
|
|
19
|
+
reasons are closed, a coverage declaration is an affordance or a reason and
|
|
20
|
+
never both, an available-affordances result is bounded, distinct and carries
|
|
21
|
+
no policy field, the error envelope and billing refusal part carry
|
|
22
|
+
affordances only when non-empty, and every new name is reachable from the
|
|
23
|
+
package root.
|
|
16
24
|
|
|
17
25
|
## Invariants
|
|
18
26
|
|
|
@@ -27,4 +35,5 @@ None — test-only.
|
|
|
27
35
|
|
|
28
36
|
## Dependencies
|
|
29
37
|
|
|
30
|
-
`vitest`, `zod`, `../index`, `../../workflows/outcome
|
|
38
|
+
`vitest`, `zod`, `../index`, `../../workflows/outcome`, `../../api/primitives`,
|
|
39
|
+
`../../billing/refusals` and the package root.
|
|
@@ -32,7 +32,7 @@ const schemaMatchesType: Equals<
|
|
|
32
32
|
> = true;
|
|
33
33
|
|
|
34
34
|
describe("affordance vocabulary", () => {
|
|
35
|
-
it("names the
|
|
35
|
+
it("names the nineteen affordances, in order", () => {
|
|
36
36
|
expect(AFFORDANCE_IDS).toEqual([
|
|
37
37
|
"sourceSync.findFiles",
|
|
38
38
|
"integrations.connectGoogleDrive",
|
|
@@ -41,6 +41,18 @@ describe("affordance vocabulary", () => {
|
|
|
41
41
|
"orgStructure.reanalyze",
|
|
42
42
|
"hris.resyncDirectory",
|
|
43
43
|
"review.open",
|
|
44
|
+
"integrations.open",
|
|
45
|
+
"hris.connect",
|
|
46
|
+
"account.editProfile",
|
|
47
|
+
"workspace.openOrgSettings",
|
|
48
|
+
"sourceSync.addFile",
|
|
49
|
+
"sourceSync.openUpdate",
|
|
50
|
+
"orgStructure.openReview",
|
|
51
|
+
"chat.open",
|
|
52
|
+
"companyMd.openDoc",
|
|
53
|
+
"companyMd.openHistory",
|
|
54
|
+
"team.open",
|
|
55
|
+
"execution.open",
|
|
44
56
|
]);
|
|
45
57
|
expect(schemaMatchesType).toBe(true);
|
|
46
58
|
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import * as root from "../../index";
|
|
5
|
+
import * as barrel from "../index";
|
|
6
|
+
import {
|
|
7
|
+
AFFORDANCE_AVAILABILITY_OF,
|
|
8
|
+
AFFORDANCE_BINDING_KIND_OF,
|
|
9
|
+
AFFORDANCE_IDS,
|
|
10
|
+
AffordanceCoverageSchema,
|
|
11
|
+
AffordanceSchema,
|
|
12
|
+
AvailableAffordancesSchema,
|
|
13
|
+
NO_AFFORDANCE_REASONS,
|
|
14
|
+
OfferedAffordanceSchema,
|
|
15
|
+
} from "../index";
|
|
16
|
+
import { ErrorResponseSchema } from "../../api/primitives";
|
|
17
|
+
import {
|
|
18
|
+
BILLING_REFUSAL_CODES,
|
|
19
|
+
BillingRefusalPartSchema,
|
|
20
|
+
} from "../../billing/refusals";
|
|
21
|
+
|
|
22
|
+
// The burn-down wave (PRD-01137): exactly these ids, appended, with these kinds.
|
|
23
|
+
const WAVE = {
|
|
24
|
+
"integrations.open": "navigate",
|
|
25
|
+
"hris.connect": "surface",
|
|
26
|
+
"account.editProfile": "navigate",
|
|
27
|
+
"workspace.openOrgSettings": "navigate",
|
|
28
|
+
"sourceSync.addFile": "surface",
|
|
29
|
+
"sourceSync.openUpdate": "surface",
|
|
30
|
+
"orgStructure.openReview": "surface",
|
|
31
|
+
"chat.open": "navigate",
|
|
32
|
+
"companyMd.openDoc": "navigate",
|
|
33
|
+
"companyMd.openHistory": "navigate",
|
|
34
|
+
"team.open": "navigate",
|
|
35
|
+
"execution.open": "navigate",
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
const TARGET_ARG = {
|
|
39
|
+
"sourceSync.openUpdate": "proposalId",
|
|
40
|
+
"orgStructure.openReview": "proposalId",
|
|
41
|
+
"chat.open": "chatId",
|
|
42
|
+
"companyMd.openDoc": "docId",
|
|
43
|
+
"companyMd.openHistory": "docId",
|
|
44
|
+
"team.open": "unitId",
|
|
45
|
+
"execution.open": "executionId",
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
describe("affordance catalog wave", () => {
|
|
49
|
+
it("appends the twelve burn-down ids after the original seven, in order", () => {
|
|
50
|
+
expect(AFFORDANCE_IDS.slice(7)).toEqual(Object.keys(WAVE));
|
|
51
|
+
expect(AFFORDANCE_IDS).toHaveLength(19);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("every new id has the locked binding kind", () => {
|
|
55
|
+
for (const [id, kind] of Object.entries(WAVE)) {
|
|
56
|
+
expect(AFFORDANCE_BINDING_KIND_OF[id as keyof typeof WAVE], id).toBe(
|
|
57
|
+
kind,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("the burn-down ids read no live availability", () => {
|
|
63
|
+
for (const id of Object.keys(WAVE)) {
|
|
64
|
+
expect(
|
|
65
|
+
AFFORDANCE_AVAILABILITY_OF[id as keyof typeof WAVE],
|
|
66
|
+
id,
|
|
67
|
+
).toBeNull();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("an id that targets one thing requires its id arg and refuses any other key", () => {
|
|
72
|
+
for (const [id, key] of Object.entries(TARGET_ARG)) {
|
|
73
|
+
expect(
|
|
74
|
+
OfferedAffordanceSchema.safeParse({ id, args: { [key]: "x1" } })
|
|
75
|
+
.success,
|
|
76
|
+
id,
|
|
77
|
+
).toBe(true);
|
|
78
|
+
expect(OfferedAffordanceSchema.safeParse({ id }).success, id).toBe(false);
|
|
79
|
+
expect(
|
|
80
|
+
OfferedAffordanceSchema.safeParse({ id, args: { [key]: "" } }).success,
|
|
81
|
+
id,
|
|
82
|
+
).toBe(false);
|
|
83
|
+
expect(
|
|
84
|
+
OfferedAffordanceSchema.safeParse({
|
|
85
|
+
id,
|
|
86
|
+
args: { [key]: "x1", url: "/x" },
|
|
87
|
+
}).success,
|
|
88
|
+
id,
|
|
89
|
+
).toBe(false);
|
|
90
|
+
}
|
|
91
|
+
for (const id of [
|
|
92
|
+
"integrations.open",
|
|
93
|
+
"hris.connect",
|
|
94
|
+
"account.editProfile",
|
|
95
|
+
"workspace.openOrgSettings",
|
|
96
|
+
"sourceSync.addFile",
|
|
97
|
+
]) {
|
|
98
|
+
expect(OfferedAffordanceSchema.safeParse({ id }).success, id).toBe(true);
|
|
99
|
+
expect(
|
|
100
|
+
OfferedAffordanceSchema.safeParse({ id, args: {} }).success,
|
|
101
|
+
id,
|
|
102
|
+
).toBe(false);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("the new instance schemas convert to JSON Schema", () => {
|
|
107
|
+
expect(() =>
|
|
108
|
+
z.toJSONSchema(AffordanceSchema, { io: "input" }),
|
|
109
|
+
).not.toThrow();
|
|
110
|
+
expect(() =>
|
|
111
|
+
z.toJSONSchema(AvailableAffordancesSchema, { io: "input" }),
|
|
112
|
+
).not.toThrow();
|
|
113
|
+
expect(() =>
|
|
114
|
+
z.toJSONSchema(AffordanceCoverageSchema, { io: "input" }),
|
|
115
|
+
).not.toThrow();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("the no-affordance reasons are closed, in order", () => {
|
|
119
|
+
expect(NO_AFFORDANCE_REASONS).toEqual([
|
|
120
|
+
"readOnly",
|
|
121
|
+
"inlineControl",
|
|
122
|
+
"internalAdmin",
|
|
123
|
+
"systemOnly",
|
|
124
|
+
"authFlow",
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("a coverage declaration names an affordance or a closed reason, never both", () => {
|
|
129
|
+
expect(
|
|
130
|
+
AffordanceCoverageSchema.safeParse({ affordance: "billing.open" })
|
|
131
|
+
.success,
|
|
132
|
+
).toBe(true);
|
|
133
|
+
expect(
|
|
134
|
+
AffordanceCoverageSchema.safeParse({ noAffordance: "authFlow" }).success,
|
|
135
|
+
).toBe(true);
|
|
136
|
+
expect(
|
|
137
|
+
AffordanceCoverageSchema.safeParse({
|
|
138
|
+
affordance: "billing.open",
|
|
139
|
+
noAffordance: "authFlow",
|
|
140
|
+
}).success,
|
|
141
|
+
).toBe(false);
|
|
142
|
+
expect(
|
|
143
|
+
AffordanceCoverageSchema.safeParse({ noAffordance: "because" }).success,
|
|
144
|
+
).toBe(false);
|
|
145
|
+
expect(AffordanceCoverageSchema.safeParse({}).success).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("an available-affordances result is bounded, distinct and policy-filtered", () => {
|
|
149
|
+
const entry = {
|
|
150
|
+
affordance: { id: "billing.open" },
|
|
151
|
+
availability: { state: "available" },
|
|
152
|
+
};
|
|
153
|
+
expect(
|
|
154
|
+
AvailableAffordancesSchema.safeParse({ affordances: [] }).success,
|
|
155
|
+
).toBe(true);
|
|
156
|
+
expect(
|
|
157
|
+
AvailableAffordancesSchema.safeParse({ affordances: [entry] }).success,
|
|
158
|
+
).toBe(true);
|
|
159
|
+
expect(
|
|
160
|
+
AvailableAffordancesSchema.safeParse({ affordances: [entry, entry] })
|
|
161
|
+
.success,
|
|
162
|
+
).toBe(false);
|
|
163
|
+
expect(
|
|
164
|
+
AvailableAffordancesSchema.safeParse({
|
|
165
|
+
affordances: [{ ...entry, policy: "allowed" }],
|
|
166
|
+
}).success,
|
|
167
|
+
).toBe(false);
|
|
168
|
+
const many = Array.from({ length: 17 }, (_, i) => ({
|
|
169
|
+
affordance: { id: "chat.open", args: { chatId: `c${i}` } },
|
|
170
|
+
availability: { state: "available" },
|
|
171
|
+
}));
|
|
172
|
+
expect(
|
|
173
|
+
AvailableAffordancesSchema.safeParse({ affordances: many }).success,
|
|
174
|
+
).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("the error envelope and the billing refusal part offer affordances only when there are some", () => {
|
|
178
|
+
const err = (extra: Record<string, unknown>) => ({
|
|
179
|
+
error: {
|
|
180
|
+
code: "FORBIDDEN",
|
|
181
|
+
message: "Billing is paused.",
|
|
182
|
+
retryable: false,
|
|
183
|
+
...extra,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
expect(ErrorResponseSchema.safeParse(err({})).success).toBe(true);
|
|
187
|
+
expect(
|
|
188
|
+
ErrorResponseSchema.safeParse(
|
|
189
|
+
err({ affordances: [{ id: "billing.open" }] }),
|
|
190
|
+
).success,
|
|
191
|
+
).toBe(true);
|
|
192
|
+
expect(
|
|
193
|
+
ErrorResponseSchema.safeParse(err({ affordances: [] })).success,
|
|
194
|
+
).toBe(false);
|
|
195
|
+
expect(
|
|
196
|
+
ErrorResponseSchema.safeParse(
|
|
197
|
+
err({ affordances: [{ id: "billing.nope" }] }),
|
|
198
|
+
).success,
|
|
199
|
+
).toBe(false);
|
|
200
|
+
const part = {
|
|
201
|
+
code: BILLING_REFUSAL_CODES[0],
|
|
202
|
+
orgId: "org-1",
|
|
203
|
+
retryable: false,
|
|
204
|
+
};
|
|
205
|
+
expect(BillingRefusalPartSchema.safeParse(part).success).toBe(true);
|
|
206
|
+
expect(
|
|
207
|
+
BillingRefusalPartSchema.safeParse({
|
|
208
|
+
...part,
|
|
209
|
+
affordances: [{ id: "billing.open" }],
|
|
210
|
+
}).success,
|
|
211
|
+
).toBe(true);
|
|
212
|
+
expect(
|
|
213
|
+
BillingRefusalPartSchema.safeParse({ ...part, affordances: [] }).success,
|
|
214
|
+
).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("every new name is reachable from the package root as the same binding", () => {
|
|
218
|
+
for (const name of [
|
|
219
|
+
"AffordanceCoverageSchema",
|
|
220
|
+
"AvailableAffordanceSchema",
|
|
221
|
+
"AvailableAffordancesSchema",
|
|
222
|
+
"ChatOpenArgsSchema",
|
|
223
|
+
"CompanyMdDocArgsSchema",
|
|
224
|
+
"ExecutionOpenArgsSchema",
|
|
225
|
+
"NO_AFFORDANCE_REASONS",
|
|
226
|
+
"NoAffordanceReasonSchema",
|
|
227
|
+
"OrgStructureOpenReviewArgsSchema",
|
|
228
|
+
"SourceSyncOpenUpdateArgsSchema",
|
|
229
|
+
"TeamOpenArgsSchema",
|
|
230
|
+
] as const) {
|
|
231
|
+
expect((root as Record<string, unknown>)[name], name).toBeDefined();
|
|
232
|
+
expect((root as Record<string, unknown>)[name], name).toBe(
|
|
233
|
+
(barrel as Record<string, unknown>)[name],
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -14,7 +14,16 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
ChatOpenArgsSchema,
|
|
19
|
+
CompanyMdDocArgsSchema,
|
|
20
|
+
ExecutionOpenArgsSchema,
|
|
21
|
+
OrgStructureOpenReviewArgsSchema,
|
|
22
|
+
ReviewOpenArgsSchema,
|
|
23
|
+
SourceSyncOpenUpdateArgsSchema,
|
|
24
|
+
TeamOpenArgsSchema,
|
|
25
|
+
type AffordanceArgs,
|
|
26
|
+
} from "./args";
|
|
18
27
|
import type {
|
|
19
28
|
AffordanceAvailabilityId,
|
|
20
29
|
AffordanceBindingKind,
|
|
@@ -44,6 +53,11 @@ export type OfferedAffordance = Exclude<
|
|
|
44
53
|
const noArgs = <K extends AffordanceId>(id: K) =>
|
|
45
54
|
z.object({ id: z.literal(id) }).strict();
|
|
46
55
|
|
|
56
|
+
const withArgs = <K extends AffordanceId, A extends z.ZodTypeAny>(
|
|
57
|
+
id: K,
|
|
58
|
+
args: A,
|
|
59
|
+
) => z.object({ id: z.literal(id), args }).strict();
|
|
60
|
+
|
|
47
61
|
const OFFERED_MEMBERS = [
|
|
48
62
|
noArgs("sourceSync.findFiles"),
|
|
49
63
|
noArgs("integrations.connectGoogleDrive"),
|
|
@@ -51,14 +65,24 @@ const OFFERED_MEMBERS = [
|
|
|
51
65
|
noArgs("billing.open"),
|
|
52
66
|
noArgs("orgStructure.reanalyze"),
|
|
53
67
|
noArgs("hris.resyncDirectory"),
|
|
68
|
+
noArgs("integrations.open"),
|
|
69
|
+
noArgs("hris.connect"),
|
|
70
|
+
noArgs("account.editProfile"),
|
|
71
|
+
noArgs("workspace.openOrgSettings"),
|
|
72
|
+
noArgs("sourceSync.addFile"),
|
|
73
|
+
withArgs("sourceSync.openUpdate", SourceSyncOpenUpdateArgsSchema),
|
|
74
|
+
withArgs("orgStructure.openReview", OrgStructureOpenReviewArgsSchema),
|
|
75
|
+
withArgs("chat.open", ChatOpenArgsSchema),
|
|
76
|
+
withArgs("companyMd.openDoc", CompanyMdDocArgsSchema),
|
|
77
|
+
withArgs("companyMd.openHistory", CompanyMdDocArgsSchema),
|
|
78
|
+
withArgs("team.open", TeamOpenArgsSchema),
|
|
79
|
+
withArgs("execution.open", ExecutionOpenArgsSchema),
|
|
54
80
|
] as const;
|
|
55
81
|
|
|
56
82
|
/** Every affordance instance, validated per id. */
|
|
57
83
|
export const AffordanceSchema = z.discriminatedUnion("id", [
|
|
58
84
|
...OFFERED_MEMBERS,
|
|
59
|
-
|
|
60
|
-
.object({ id: z.literal("review.open"), args: ReviewOpenArgsSchema })
|
|
61
|
-
.strict(),
|
|
85
|
+
withArgs("review.open", ReviewOpenArgsSchema),
|
|
62
86
|
]);
|
|
63
87
|
|
|
64
88
|
/** What may be persisted inside an offer: every member except a projection. */
|
|
@@ -76,6 +100,18 @@ export const AFFORDANCE_BINDING_KIND_OF = {
|
|
|
76
100
|
"orgStructure.reanalyze": "command",
|
|
77
101
|
"hris.resyncDirectory": "command",
|
|
78
102
|
"review.open": "surface",
|
|
103
|
+
"integrations.open": "navigate",
|
|
104
|
+
"hris.connect": "surface",
|
|
105
|
+
"account.editProfile": "navigate",
|
|
106
|
+
"workspace.openOrgSettings": "navigate",
|
|
107
|
+
"sourceSync.addFile": "surface",
|
|
108
|
+
"sourceSync.openUpdate": "surface",
|
|
109
|
+
"orgStructure.openReview": "surface",
|
|
110
|
+
"chat.open": "navigate",
|
|
111
|
+
"companyMd.openDoc": "navigate",
|
|
112
|
+
"companyMd.openHistory": "navigate",
|
|
113
|
+
"team.open": "navigate",
|
|
114
|
+
"execution.open": "navigate",
|
|
79
115
|
} as const satisfies Record<AffordanceId, AffordanceBindingKind>;
|
|
80
116
|
|
|
81
117
|
/** The live condition each affordance's availability is read from, if any. */
|
|
@@ -88,6 +124,22 @@ export const AFFORDANCE_AVAILABILITY_OF = {
|
|
|
88
124
|
"orgStructure.reanalyze": null,
|
|
89
125
|
"hris.resyncDirectory": "hris.connected",
|
|
90
126
|
"review.open": null,
|
|
127
|
+
// The burn-down wave reads no live condition: a navigate target is always
|
|
128
|
+
// reachable (its page renders its own state), and the two surfaces open a
|
|
129
|
+
// dialog that shows whether the HR system is connected / which files are
|
|
130
|
+
// already synced. A later availability id is an append, never a change.
|
|
131
|
+
"integrations.open": null,
|
|
132
|
+
"hris.connect": null,
|
|
133
|
+
"account.editProfile": null,
|
|
134
|
+
"workspace.openOrgSettings": null,
|
|
135
|
+
"sourceSync.addFile": null,
|
|
136
|
+
"sourceSync.openUpdate": null,
|
|
137
|
+
"orgStructure.openReview": null,
|
|
138
|
+
"chat.open": null,
|
|
139
|
+
"companyMd.openDoc": null,
|
|
140
|
+
"companyMd.openHistory": null,
|
|
141
|
+
"team.open": null,
|
|
142
|
+
"execution.open": null,
|
|
91
143
|
} as const satisfies Record<AffordanceId, AffordanceAvailabilityId | null>;
|
|
92
144
|
|
|
93
145
|
/**
|
package/src/affordances/args.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The arguments each affordance carries. An affordance INSTANCE is
|
|
3
3
|
* `{ id, args }`, never a bare id, so an affordance that one day needs a
|
|
4
4
|
* target (a document, an import, a finding) is a new member here, not a new
|
|
5
|
-
* persisted shape.
|
|
5
|
+
* persisted shape. An org-scoped action takes no arguments; an action on one
|
|
6
|
+
* thing names it by its id, never by a URL (the binding owns the route).
|
|
6
7
|
*/
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
|
|
@@ -17,6 +18,36 @@ export const ReviewOpenArgsSchema = z
|
|
|
17
18
|
})
|
|
18
19
|
.strict();
|
|
19
20
|
|
|
21
|
+
/** `sourceSync.openUpdate`: which Source Sync update (proposal). */
|
|
22
|
+
export const SourceSyncOpenUpdateArgsSchema = z
|
|
23
|
+
.object({ proposalId: z.string().min(1) })
|
|
24
|
+
.strict();
|
|
25
|
+
|
|
26
|
+
/** `orgStructure.openReview`: which org-structure proposal's chat review. */
|
|
27
|
+
export const OrgStructureOpenReviewArgsSchema = z
|
|
28
|
+
.object({ proposalId: z.string().min(1) })
|
|
29
|
+
.strict();
|
|
30
|
+
|
|
31
|
+
/** `chat.open`: which chat. */
|
|
32
|
+
export const ChatOpenArgsSchema = z
|
|
33
|
+
.object({ chatId: z.string().min(1) })
|
|
34
|
+
.strict();
|
|
35
|
+
|
|
36
|
+
/** `companyMd.openDoc` / `companyMd.openHistory`: which document. */
|
|
37
|
+
export const CompanyMdDocArgsSchema = z
|
|
38
|
+
.object({ docId: z.string().min(1) })
|
|
39
|
+
.strict();
|
|
40
|
+
|
|
41
|
+
/** `team.open`: which team (org unit). */
|
|
42
|
+
export const TeamOpenArgsSchema = z
|
|
43
|
+
.object({ unitId: z.string().min(1) })
|
|
44
|
+
.strict();
|
|
45
|
+
|
|
46
|
+
/** `execution.open`: which execution. */
|
|
47
|
+
export const ExecutionOpenArgsSchema = z
|
|
48
|
+
.object({ executionId: z.string().min(1) })
|
|
49
|
+
.strict();
|
|
50
|
+
|
|
20
51
|
/**
|
|
21
52
|
* Argument schemas, ONLY for ids that take arguments. An id that takes none is
|
|
22
53
|
* listed in NO_ARG_AFFORDANCE_IDS and its instance OMITS `args` entirely: there
|
|
@@ -27,6 +58,13 @@ export const ReviewOpenArgsSchema = z
|
|
|
27
58
|
*/
|
|
28
59
|
export const AFFORDANCE_ARGS = {
|
|
29
60
|
"review.open": ReviewOpenArgsSchema,
|
|
61
|
+
"sourceSync.openUpdate": SourceSyncOpenUpdateArgsSchema,
|
|
62
|
+
"orgStructure.openReview": OrgStructureOpenReviewArgsSchema,
|
|
63
|
+
"chat.open": ChatOpenArgsSchema,
|
|
64
|
+
"companyMd.openDoc": CompanyMdDocArgsSchema,
|
|
65
|
+
"companyMd.openHistory": CompanyMdDocArgsSchema,
|
|
66
|
+
"team.open": TeamOpenArgsSchema,
|
|
67
|
+
"execution.open": ExecutionOpenArgsSchema,
|
|
30
68
|
} as const;
|
|
31
69
|
|
|
32
70
|
/** The ids whose instances carry no `args` key. */
|
|
@@ -37,6 +75,11 @@ export const NO_ARG_AFFORDANCE_IDS = [
|
|
|
37
75
|
"billing.open",
|
|
38
76
|
"orgStructure.reanalyze",
|
|
39
77
|
"hris.resyncDirectory",
|
|
78
|
+
"integrations.open",
|
|
79
|
+
"hris.connect",
|
|
80
|
+
"account.editProfile",
|
|
81
|
+
"workspace.openOrgSettings",
|
|
82
|
+
"sourceSync.addFile",
|
|
40
83
|
] as const satisfies readonly AffordanceId[];
|
|
41
84
|
|
|
42
85
|
type ArgsId = keyof typeof AFFORDANCE_ARGS;
|