@company-semantics/contracts 59.1.0 → 61.0.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": "59.1.0",
3
+ "version": "61.0.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
2
- export const SPEC_HASH = 'dd2a725d7ebd' as const;
3
- export const SPEC_HASH_FULL = 'dd2a725d7ebd694db25efbe0e638077984af41551c1989fb374d9a7b8bc4eb3d' as const;
2
+ export const SPEC_HASH = '4ee1bc3befbf' as const;
3
+ export const SPEC_HASH_FULL = '4ee1bc3befbf8bb865b4bdad38b892c59f198fec45acdcdde93a067a678516b3' as const;
@@ -5065,7 +5065,7 @@ export interface components {
5065
5065
  summary: {
5066
5066
  executionId: string;
5067
5067
  /** @enum {string} */
5068
- kind: "integration.connect" | "integration.disconnect" | "integration.purge" | "profile.update" | "slack.send" | "data.ingest" | "data.scope" | "system.cleanup" | "member.changeManager" | "companyMd.ingest" | "companyMd.commentPass" | "semantic.transform" | "companyMd.importContextDoc";
5068
+ kind: "integration.connect" | "integration.disconnect" | "integration.purge" | "profile.update" | "slack.send" | "data.ingest" | "data.scope" | "system.cleanup" | "member.changeManager" | "companyMd.ingest" | "companyMd.commentPass" | "companyMd.commentAction" | "semantic.transform" | "companyMd.importContextDoc";
5069
5069
  target: {
5070
5070
  /** @constant */
5071
5071
  type: "slack";
@@ -5313,6 +5313,7 @@ export interface components {
5313
5313
  canUploadContext?: boolean;
5314
5314
  canManageAcl?: boolean;
5315
5315
  canComment: boolean;
5316
+ canDecideSuggestion: boolean;
5316
5317
  canChangeVisibility?: boolean;
5317
5318
  inheritsFromId: string | null;
5318
5319
  inheritsFrom: string | null;
@@ -6992,6 +6993,7 @@ export interface components {
6992
6993
  createdAt: string;
6993
6994
  updatedAt: string;
6994
6995
  comments: components["schemas"]["Comment"][];
6996
+ capabilities: components["schemas"]["ThreadCapabilities"];
6995
6997
  };
6996
6998
  /** @description Opaque, versioned comment anchor: document-level, a text anchor with a required quote and optional relative positions, or a zero-width text-insertion point for insert suggestions. */
6997
6999
  CommentAnchor: {
@@ -7046,6 +7048,14 @@ export interface components {
7046
7048
  endOffset: number | null;
7047
7049
  displayName: string;
7048
7050
  };
7051
+ /** @description What the requesting actor may do to one thread, decided server-side. */
7052
+ ThreadCapabilities: {
7053
+ canReply: boolean;
7054
+ canResolve: boolean;
7055
+ canReopen: boolean;
7056
+ canDecideSuggestion: boolean;
7057
+ canWithdraw: boolean;
7058
+ };
7049
7059
  /** @description A comment thread without its comments. */
7050
7060
  CommentThreadSummary: {
7051
7061
  id: string;
@@ -9116,7 +9126,7 @@ export interface operations {
9116
9126
  query?: {
9117
9127
  cursor?: string;
9118
9128
  limit?: number;
9119
- kind?: "integration.connect" | "integration.disconnect" | "integration.purge" | "profile.update" | "slack.send" | "data.ingest" | "data.scope" | "system.cleanup" | "member.changeManager" | "companyMd.ingest" | "companyMd.commentPass" | "semantic.transform" | "companyMd.importContextDoc";
9129
+ kind?: "integration.connect" | "integration.disconnect" | "integration.purge" | "profile.update" | "slack.send" | "data.ingest" | "data.scope" | "system.cleanup" | "member.changeManager" | "companyMd.ingest" | "companyMd.commentPass" | "companyMd.commentAction" | "semantic.transform" | "companyMd.importContextDoc";
9120
9130
  targetType?: string;
9121
9131
  periodStart?: string;
9122
9132
  periodEnd?: string;
@@ -9251,7 +9261,7 @@ export interface operations {
9251
9261
  query?: {
9252
9262
  limit?: number;
9253
9263
  offset?: number;
9254
- kind?: "integration.connect" | "integration.disconnect" | "integration.purge" | "profile.update" | "slack.send" | "data.ingest" | "data.scope" | "system.cleanup" | "member.changeManager" | "companyMd.ingest" | "companyMd.commentPass" | "semantic.transform" | "companyMd.importContextDoc";
9264
+ kind?: "integration.connect" | "integration.disconnect" | "integration.purge" | "profile.update" | "slack.send" | "data.ingest" | "data.scope" | "system.cleanup" | "member.changeManager" | "companyMd.ingest" | "companyMd.commentPass" | "companyMd.commentAction" | "semantic.transform" | "companyMd.importContextDoc";
9255
9265
  kindPrefix?: string;
9256
9266
  };
9257
9267
  header?: never;
@@ -73,6 +73,17 @@ function makeThread(over: Record<string, unknown> = {}) {
73
73
  createdAt: "2026-08-01T12:00:00.000Z",
74
74
  updatedAt: "2026-08-01T12:00:00.000Z",
75
75
  comments: [makeComment()],
76
+ // REQUIRED on a thread carried WITH its comments. The summary shape has no
77
+ // capabilities and no comments — a `CommentThreadSummary` parse of this
78
+ // fixture simply ignores both, which is why the summary cases below still
79
+ // pass unchanged.
80
+ capabilities: {
81
+ canReply: true,
82
+ canResolve: true,
83
+ canReopen: false,
84
+ canDecideSuggestion: false,
85
+ canWithdraw: false,
86
+ },
76
87
  ...over,
77
88
  };
78
89
  }
@@ -216,8 +216,58 @@ export type CommentThreadSummary = z.infer<typeof CommentThreadSummarySchema>;
216
216
  * order a redacted row used to hold open no longer has anything to hold. The
217
217
  * wire SHAPE is unchanged either way; what changed is which rows arrive.
218
218
  */
219
+ /**
220
+ * What the requesting actor may do to ONE thread.
221
+ *
222
+ * SERVER-COMPUTED, and that is the point. The app used to reconstruct this
223
+ * model client-side from doc-level signals plus the viewer's id, which is how
224
+ * the two surfaces came to disagree — the agent's read reported `canResolve:
225
+ * false` to actors who had a working Resolve button in the rail. One predicate,
226
+ * on the server, answering for every surface.
227
+ *
228
+ * Each flag ANDs the actor's subject-level band with the thread's own kind and
229
+ * status, so a flag is `true` only where the verb would actually be admitted.
230
+ * Two things it deliberately cannot express, both documented rather than
231
+ * papered over:
232
+ * - a LIVE ACCEPTANCE LEASE, which makes both decide verbs 409 while
233
+ * `suggestion.status` is still `open`; the claim columns are never
234
+ * projected, so `canDecideSuggestion: true` is "band and status permit",
235
+ * not "nobody else is mid-accept".
236
+ * - anything about OTHER threads; these are per-thread answers.
237
+ */
238
+ export const ThreadCapabilitiesSchema = z.object({
239
+ /** Write a reply. Not conditioned on status — a reply reopens a resolved thread. */
240
+ canReply: z.boolean(),
241
+ /** Resolve it: an OPEN comment thread only; a suggestion is decided, not resolved. */
242
+ canResolve: z.boolean(),
243
+ /** Reopen it: the exact complement of `canResolve`. */
244
+ canReopen: z.boolean(),
245
+ /** Accept or reject an OPEN suggestion — the edit band (ADR-BE-536). */
246
+ canDecideSuggestion: z.boolean(),
247
+ /**
248
+ * Withdraw one's OWN open suggestion. IDENTITY, not band: the author needs no
249
+ * edit capability, so this is true for authors that `canDecideSuggestion`
250
+ * refuses. Separate for exactly that reason — folded together, the stricter
251
+ * answer governed and a commenter-band author was told they could not close
252
+ * their own suggestion.
253
+ */
254
+ canWithdraw: z.boolean(),
255
+ });
256
+ export type ThreadCapabilities = z.infer<typeof ThreadCapabilitiesSchema>;
257
+
219
258
  export const CommentThreadSchema = CommentThreadSummarySchema.extend({
220
259
  comments: z.array(CommentSchema),
260
+ /**
261
+ * REQUIRED. Every route that returns a thread WITH its comments computes it —
262
+ * the list read and the create; the mutation routes return a
263
+ * `CommentThreadSummary`, which carries no comments and no capabilities.
264
+ *
265
+ * Not optional, deliberately. An optional block means a client keeps a local
266
+ * derivation for when it is absent, and that second opinion is the precise
267
+ * thing this field exists to delete: the app and the agent-facing read had
268
+ * drifted into disagreeing about the same thread.
269
+ */
270
+ capabilities: ThreadCapabilitiesSchema,
221
271
  });
222
272
  export type CommentThread = z.infer<typeof CommentThreadSchema>;
223
273
 
@@ -202,6 +202,29 @@ export const CompanyMdDocResponseSchema = z.object({
202
202
  * takes the major bump.
203
203
  */
204
204
  canComment: z.boolean(),
205
+ /**
206
+ * SUGGESTION-DECIDE capability signal: whether the requesting actor holds
207
+ * `CompanyMd.CanEdit` on this doc as `assertCanDecideSuggestion` evaluates it
208
+ * — accepting or rejecting someone's suggestion arbitrates what the document
209
+ * will say, so it rides the edit band (PRD-00934 / ADR-BE-536).
210
+ *
211
+ * IT EXISTS BECAUSE `canEdit` IS NOT ONE ANSWER. The app used to derive the
212
+ * Accept/Reject affordance from `canEdit`, which is the BATCH path
213
+ * (`canUserEditDocBatch`): it subtracts docs whose projection is stale and
214
+ * adds owner / account-owner identity safeties. The gate the Accept button
215
+ * actually hits is `evaluate('CompanyMd.CanEdit')`, which has neither. The two
216
+ * disagree in both directions — a dead Accept button on a doc whose projection
217
+ * has not compiled, and a hidden affordance on one whose access shrank but has
218
+ * not recompiled. Same nominal policy, two implementations; this field is the
219
+ * one the gate uses, computed the way the gate computes it.
220
+ *
221
+ * REQUIRED, like `canComment` above and unlike the three sharing signals: an
222
+ * absent value would mean "unknown", and a client that invents a default for
223
+ * an authority question has replaced the server's answer with a guess. The
224
+ * single-doc read is the only producer of this shape and always computes it.
225
+ * Signal only; admittance and enforcement are unchanged.
226
+ */
227
+ canDecideSuggestion: z.boolean(),
205
228
  /**
206
229
  * GOVERNANCE capability signal (ADR-BE-435): whether the requesting actor holds
207
230
  * `CompanyMd.CanChangeVisibility` (owner OR the `org.manage_content_sharing`
@@ -71,7 +71,29 @@ export type DecisionEffect =
71
71
  * derivation to run again. Carries the reason so the follow-up has something
72
72
  * to act on, and so the reviewer is not told "saved" when nothing moved.
73
73
  */
74
- | { readonly kind: "needs_reinference"; readonly note: string };
74
+ | { readonly kind: "needs_reinference"; readonly note: string }
75
+ /**
76
+ * Mark a person as the head of a DURABLE unit, and optionally set that unit's
77
+ * owner label. Today the only producer names the org root, so accepting is how
78
+ * an imported chart's CEO becomes the workspace's root owner.
79
+ *
80
+ * DURABLE addressing, not a temp id: post-apply every unit is durable, so a
81
+ * real id is the general form. A temp id would serve only corrections to
82
+ * not-yet-created units, which a proposal's own `headPersonId` already covers
83
+ * with no effect at all.
84
+ *
85
+ * THIS EFFECT NEVER MINTS AUTHORITY. It writes a presentation/semantic marker
86
+ * and a label. Granting unit or account ownership stays with the human-gated
87
+ * ownership route — a root-unit owner IS an account owner, so an automated
88
+ * grant would be a privilege-escalation path.
89
+ */
90
+ | {
91
+ readonly kind: "set_unit_head";
92
+ readonly unitId: string;
93
+ readonly personId: string;
94
+ /** Non-null also sets the unit's owner label; null marks the head only. */
95
+ readonly title: string | null;
96
+ };
75
97
 
76
98
  /** The closed set of effect kinds, as runtime data for validators. */
77
99
  export const DECISION_EFFECT_KINDS = [
@@ -81,6 +103,7 @@ export const DECISION_EFFECT_KINDS = [
81
103
  "unadopt_unit",
82
104
  "archive_unit",
83
105
  "needs_reinference",
106
+ "set_unit_head",
84
107
  ] as const satisfies ReadonlyArray<DecisionEffect["kind"]>;
85
108
 
86
109
  /** One answer a human may give, with what it means and what it does. */
@@ -243,6 +243,27 @@ describe("EXECUTION_KINDS golden snapshot", () => {
243
243
  templateId: "companyMd.commentPass",
244
244
  },
245
245
  },
246
+ "companyMd.commentAction": {
247
+ kind: "companyMd.commentAction",
248
+ domain: "data",
249
+ display: {
250
+ label: "Apply Comment Action",
251
+ pastTenseLabel: "Comment action applied",
252
+ icon: "pencil",
253
+ },
254
+ governance: {
255
+ visibility: "user",
256
+ requiresAdmin: false,
257
+ },
258
+ ui: {
259
+ showInAdmin: false,
260
+ showInTimeline: true,
261
+ confirmBeforeRun: true,
262
+ },
263
+ explanation: {
264
+ templateId: "companyMd.commentAction",
265
+ },
266
+ },
246
267
  "semantic.transform": {
247
268
  kind: "semantic.transform",
248
269
  domain: "organization",
@@ -300,12 +321,14 @@ describe("ExecutionKind exhaustiveness", () => {
300
321
  });
301
322
 
302
323
  it("every kind the backend dispatches is a union member, not a call-site cast", () => {
303
- // These three previously ran while cast to ExecutionKind at their call
304
- // sites, which left the registry incomplete over what actually executes.
324
+ // These previously ran or, for the comment ACTION, previewed a confirmable
325
+ // intent that then routed nowhere while cast to ExecutionKind at their
326
+ // call sites, which left the registry incomplete over what actually executes.
305
327
  for (const kind of [
306
328
  "companyMd.commentPass",
307
329
  "semantic.transform",
308
330
  "companyMd.importContextDoc",
331
+ "companyMd.commentAction",
309
332
  ]) {
310
333
  expect(isValidExecutionKind(kind)).toBe(true);
311
334
  expect(CONFIRMATION_LABELS[kind as ExecutionKind]).toBeTruthy();
@@ -55,4 +55,12 @@ export type ExecutionKind =
55
55
  | "semantic.transform"
56
56
  /** Already RUNS today, previously cast at the call site. Creates a context
57
57
  * doc from submitted text and links it under a destination doc. */
58
- | "companyMd.importContextDoc";
58
+ | "companyMd.importContextDoc"
59
+ /** ONE collaboration write on a company.md thread — resolve, reopen, reply,
60
+ * or decide a suggestion. Distinct from `companyMd.commentPass`: the pass is
61
+ * a reviewed BUNDLE gated on the whole rail's staleness token, which is the
62
+ * wrong gate for a single act — an unrelated new comment elsewhere on the
63
+ * document would refuse it. Promoted out of a call-site cast for the same
64
+ * reason `semantic.transform` was: a kind that runs but is absent from the
65
+ * registry is invisible to every reader of it. */
66
+ | "companyMd.commentAction";
@@ -271,6 +271,29 @@ export const EXECUTION_KINDS = {
271
271
  templateId: "companyMd.commentPass",
272
272
  },
273
273
  },
274
+ "companyMd.commentAction": {
275
+ kind: "companyMd.commentAction",
276
+ domain: "data",
277
+ display: {
278
+ label: "Apply Comment Action",
279
+ pastTenseLabel: "Comment action applied",
280
+ icon: "pencil",
281
+ },
282
+ governance: {
283
+ visibility: "user",
284
+ requiresAdmin: false,
285
+ },
286
+ ui: {
287
+ showInAdmin: false,
288
+ showInTimeline: true,
289
+ // A single collaboration write still changes what the conversation says,
290
+ // and resolving or deciding is not silently reversible for other readers.
291
+ confirmBeforeRun: true,
292
+ },
293
+ explanation: {
294
+ templateId: "companyMd.commentAction",
295
+ },
296
+ },
274
297
  "semantic.transform": {
275
298
  kind: "semantic.transform",
276
299
  domain: "organization",
@@ -123,6 +123,7 @@ describe("CONFIRMATION_LABELS", () => {
123
123
  "member.changeManager": "Change Reporting Manager",
124
124
  "companyMd.ingest": "Ingest Company Knowledge",
125
125
  "companyMd.commentPass": "Apply Comment Pass",
126
+ "companyMd.commentAction": "Apply Comment Action",
126
127
  "semantic.transform": "Apply Reporting Transformation",
127
128
  "companyMd.importContextDoc": "Import Context Document",
128
129
  });
@@ -48,6 +48,7 @@ export const CONFIRMATION_LABELS: Record<ExecutionKind, string> = {
48
48
  "member.changeManager": "Change Reporting Manager",
49
49
  "companyMd.ingest": "Ingest Company Knowledge",
50
50
  "companyMd.commentPass": "Apply Comment Pass",
51
+ "companyMd.commentAction": "Apply Comment Action",
51
52
  "semantic.transform": "Apply Reporting Transformation",
52
53
  "companyMd.importContextDoc": "Import Context Document",
53
54
  };