@company-semantics/contracts 88.2.0 → 89.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.
@@ -11,6 +11,9 @@
11
11
  * - `decoy` is a source class; `unknown` is a sharing breadth
12
12
  * - No member string appears in two sets
13
13
  * - The four relevance reason code sets are pairwise disjoint
14
+ * - DOCUMENT_TYPES has exactly its thirty members, `unknown` last; it sits
15
+ * OUTSIDE the disjointness table and shares a word with another set only
16
+ * where pinned (a structural name on a separate axis)
14
17
  */
15
18
 
16
19
  import { describe, it, expect } from "vitest";
@@ -33,6 +36,7 @@ import {
33
36
 
34
37
  import {
35
38
  AUDIENCE_FITS,
39
+ DOCUMENT_TYPES,
36
40
  SHARING_BREADTHS,
37
41
  SOURCE_CLASSES,
38
42
  SOURCE_SENSITIVITY_CLASSES,
@@ -175,12 +179,24 @@ const VOCABULARIES = [
175
179
  "folder_truncated",
176
180
  "explored_t1",
177
181
  "explored_t2",
182
+ "peek_insufficient_evidence",
183
+ "type_disagreement",
184
+ "type_below_confidence",
185
+ "execution_failed_t1",
186
+ "execution_failed_t2",
178
187
  ],
179
188
  },
180
189
  {
181
190
  name: "MODEL_REASON_CODES",
182
191
  tuple: MODEL_REASON_CODES,
183
- members: ["title_path_score", "peek_confirms", "peek_contradicts"],
192
+ members: [
193
+ "title_path_score",
194
+ "peek_confirms",
195
+ "peek_contradicts",
196
+ "type_policy",
197
+ "peek_strategy_evidence_present",
198
+ "peek_strategy_evidence_absent",
199
+ ],
184
200
  },
185
201
  {
186
202
  name: "TARGET_AUDIENCES",
@@ -277,3 +293,59 @@ describe("Source Sync closed sets", () => {
277
293
  });
278
294
  });
279
295
  });
296
+
297
+ describe("document types", () => {
298
+ it("has exactly the thirty document types, in order", () => {
299
+ expect([...DOCUMENT_TYPES]).toEqual([
300
+ "board_deck",
301
+ "board_minutes",
302
+ "strategy_memo",
303
+ "planning_doc",
304
+ "okr_doc",
305
+ "roadmap",
306
+ "operating_review",
307
+ "all_hands_deck",
308
+ "offsite_notes",
309
+ "investor_update",
310
+ "pitch_deck",
311
+ "product_spec",
312
+ "research_analysis",
313
+ "general_meeting_notes",
314
+ "financial_plan",
315
+ "financial_actuals",
316
+ "equity_admin_record",
317
+ "corporate_formation_record",
318
+ "corporate_governance_record",
319
+ "government_filing",
320
+ "equity_agreement",
321
+ "commercial_agreement",
322
+ "employment_agreement",
323
+ "transaction_record",
324
+ "personal_document",
325
+ "social_event_material",
326
+ "coursework",
327
+ "empty_template",
328
+ "image_or_screenshot",
329
+ "unknown",
330
+ ]);
331
+ expect(new Set(DOCUMENT_TYPES).size).toBe(30);
332
+ });
333
+
334
+ it("unknown is the last document type", () => {
335
+ expect(DOCUMENT_TYPES[DOCUMENT_TYPES.length - 1]).toBe("unknown");
336
+ });
337
+
338
+ it("document types share a member with another set only where pinned", () => {
339
+ const others = new Set<string>(
340
+ VOCABULARIES.flatMap(({ tuple }) => [...tuple]),
341
+ );
342
+ const shared = DOCUMENT_TYPES.filter((t) => others.has(t)).sort();
343
+ expect(shared).toEqual([
344
+ "board_deck",
345
+ "board_minutes",
346
+ "investor_update",
347
+ "roadmap",
348
+ "unknown",
349
+ ]);
350
+ });
351
+ });
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The Source Sync candidate item and its one-edit patch.
3
+ *
4
+ * A candidate item is ONE source proposed for ONE target document, because
5
+ * treatment and landing are source x target (INV-SOURCESYNC-LANDING-PER-TARGET).
6
+ * It carries facts ABOUT a source and a COUNT of people it is shared with, never
7
+ * its content and never their addresses. Absent values are `.nullable()`, never
8
+ * `.optional()`, so a response has one stable shape.
9
+ *
10
+ * The patch is a request body and is `.strict()`: an unknown key is an ERROR.
11
+ *
12
+ * Split out of `schemas.ts` (which keeps the set response that lists items) so
13
+ * that file stays within its size envelope.
14
+ */
15
+
16
+ import { z } from "zod";
17
+ import { IsoDateTime } from "../api/primitives";
18
+ import {
19
+ AUDIENCE_FITS,
20
+ DOCUMENT_TYPES,
21
+ SHARING_BREADTHS,
22
+ SOURCE_CLASSES,
23
+ SOURCE_SENSITIVITY_CLASSES,
24
+ SOURCE_USES,
25
+ } from "./vocabulary";
26
+ import {
27
+ CANDIDATE_MACHINE_STATES,
28
+ CandidateAttentionSchema,
29
+ CLASSIFICATION_STATES,
30
+ PERSON_HOLD_REASONS,
31
+ refineCandidateRelevance,
32
+ RelevanceReasonSchema,
33
+ SELECTION_SOURCES,
34
+ SOURCE_RELEVANCE_TIERS,
35
+ TARGET_AUDIENCES,
36
+ } from "./relevance";
37
+
38
+ /** ONE source proposed for ONE target document. */
39
+ export const SourceSyncCandidateItemSchema = z
40
+ .object({
41
+ itemId: z.string().uuid(),
42
+ sourceId: z.string().uuid(),
43
+ /**
44
+ * Display facts about the source. Never its content. When `withheld`, `name` is
45
+ * product copy naming only the source's class and period.
46
+ */
47
+ name: z.string().min(1).max(500),
48
+ mimeType: z.string().min(1).max(255),
49
+ /** Opens the source IN THE PROVIDER, which remains the ACL authority for it. */
50
+ webViewLink: z.string().url().nullable(),
51
+ ownerLabel: z.string().max(255).nullable(),
52
+ modifiedTime: IsoDateTime.nullable(),
53
+ /**
54
+ * True when the caller may not open this source: its identifying fields are
55
+ * withheld (`webViewLink`, `ownerLabel` and `sharedWithCount` are null, and
56
+ * `name` and `matchReason` are product copy). What the approver decides stays.
57
+ */
58
+ withheld: z.boolean(),
59
+ /** 'company' or 'unit:<uuid>'. */
60
+ scopeKey: z.string().min(1).max(64),
61
+ targetDocId: z.string().uuid(),
62
+ targetDocTitle: z.string().min(1).max(255),
63
+ /** The funnel's verdict for the wire: `dropped` never reaches it. Hidden = behind the drawer, undecided. */
64
+ visibility: z.enum(SOURCE_RELEVANCE_TIERS).exclude(["dropped"]),
65
+ /** Separate from visibility: a shown line may still await classification (machineState pending_classification). */
66
+ classificationState: z.enum(CLASSIFICATION_STATES),
67
+ /** NULL until a classifier has run for this line. */
68
+ sourceClass: z.enum(SOURCE_CLASSES).nullable(),
69
+ /**
70
+ * The RESOLVED document type: what the source IS, after the backend reconciles the model's T1 and
71
+ * T2 observations deterministically (control ADR source-discovery-observation-vs-policy). A NAME,
72
+ * never a verdict — the relevance policy it maps to is backend product policy and never on the wire.
73
+ * NULL on an unclassified line and on every line of a v1-pinned run.
74
+ */
75
+ documentType: z.enum(DOCUMENT_TYPES).nullable(),
76
+ /** Confidence of the resolved type, 0..1. NULL exactly when documentType is null or `unknown`. */
77
+ documentTypeConfidence: z.number().min(0).max(1).nullable(),
78
+ periodLabel: z.string().max(64).nullable(),
79
+ seriesRole: z.enum(["current", "history"]).nullable(),
80
+ /** The FUSED relevance score (control ADR source-discovery-relevance-funnel), 0..1. */
81
+ relevance: z.number().min(0).max(1),
82
+ sensitivityClass: z.enum(SOURCE_SENSITIVITY_CLASSES),
83
+ sharingBreadth: z.enum(SHARING_BREADTHS),
84
+ /** A COUNT of people the provider shares it with, when observed. Never addresses. */
85
+ sharedWithCount: z.number().int().min(0).nullable(),
86
+ /**
87
+ * Whether attaching under THIS target would show the source to more people than the provider does.
88
+ * A REQUIRED key with a NULLABLE value. NULL = not computed yet: discovery writes it null and the
89
+ * server computes it per source x target when it builds the draft set item. Every consumer reads
90
+ * null as UNRESOLVED (fail closed) — never as 'fits'.
91
+ */
92
+ audienceFit: z.enum(AUDIENCE_FITS).nullable(),
93
+ /** Machine state: the sensitivity default. NULL until classified — never `excluded` as a stand-in. */
94
+ recommendedUse: z.enum(SOURCE_USES).nullable(),
95
+ /** The COMMITTED treatment: null until a person (or an accepted approval default) commits one. */
96
+ use: z.enum(SOURCE_USES).nullable(),
97
+ /** Provenance of `use`; null exactly when `use` is null (INV-SOURCESYNC-RECOMMENDATION-IS-NOT-COMMITMENT). */
98
+ selectionSource: z.enum(SELECTION_SOURCES).nullable(),
99
+ /** Field-level write barriers a person placed on this line (INV-SOURCESYNC-HOLDS-ARE-FIELD-BARRIERS). */
100
+ personHoldReasons: z.array(z.enum(PERSON_HOLD_REASONS)).max(4),
101
+ /** The line's machine lifecycle (INV-SOURCESYNC-SET-IS-RECONCILED). */
102
+ machineState: z.enum(CANDIDATE_MACHINE_STATES),
103
+ /** Structured reasons for `relevance`; `matchReason` is rendered from these by the server. */
104
+ relevanceReasons: z.array(RelevanceReasonSchema).max(16),
105
+ /** The audience Attach would disclose to. NULL = unknown — reads as MAY WIDEN (INV-SOURCESYNC-ATTACH-SHOWS-ITS-AUDIENCE). */
106
+ targetAudience: z.enum(TARGET_AUDIENCES).nullable(),
107
+ /** Which lane earned each model tier's attention (INV-SOURCESYNC-EXPLORATION-RESERVED). */
108
+ attention: CandidateAttentionSchema,
109
+ /**
110
+ * Whether this line is in the set that approval would land — the SERVER's
111
+ * answer, never derived from `use`. A decoy, a series' history or a line a
112
+ * person left out is false while its `use` is still null.
113
+ */
114
+ included: z.boolean(),
115
+ /** True while a `widens` item — or one whose fit is not computed yet — has no person-chosen use: approval is refused until none remain. */
116
+ treatmentUnresolved: z.boolean(),
117
+ /**
118
+ * Product-owned copy saying why this file was proposed, rendered by the server from
119
+ * `relevanceReasons`. Never model-authored prose. Deprecated: removed next release.
120
+ */
121
+ matchReason: z.string().min(1).max(500),
122
+ /** Non-null when the source could not be read (a scanned PDF, an unsupported kind). */
123
+ unreadableReason: z
124
+ .enum(["unsupported_type", "needs_ocr", "unsupported_kind"])
125
+ .nullable(),
126
+ })
127
+ .superRefine(refineCandidateRelevance);
128
+
129
+ /**
130
+ * One edit to one candidate item: a PATCH names EXACTLY ONE thing that changes, so bringing
131
+ * a line into review never also commits a treatment.
132
+ * The SERVER bumps the set's revision on every accepted edit; the client never sends one here.
133
+ */
134
+ export const SourceSyncCandidateItemPatchSchema = z
135
+ .object({
136
+ /** Bring into review: hidden → shown. Changes VISIBILITY ONLY (INV-SOURCESYNC-RECOMMENDATION-IS-NOT-COMMITMENT). */
137
+ visibility: z.literal("shown").optional(),
138
+ /** Attach, direction only, or leave out. Choosing one on a `widens` item RESOLVES its treatment. */
139
+ use: z.enum(SOURCE_USES).optional(),
140
+ /** Correct the class the classifier chose. */
141
+ sourceClass: z.enum(SOURCE_CLASSES).optional(),
142
+ /** Re-route to another target document. The server re-computes audienceFit for the new target. */
143
+ targetDocId: z.string().uuid().optional(),
144
+ })
145
+ .strict()
146
+ .refine((patch) => Object.keys(patch).length === 1, {
147
+ message: "a patch changes exactly one thing",
148
+ });
149
+
150
+ export type SourceSyncCandidateItem = z.infer<
151
+ typeof SourceSyncCandidateItemSchema
152
+ >;
153
+ export type SourceSyncCandidateItemPatch = z.infer<
154
+ typeof SourceSyncCandidateItemPatchSchema
155
+ >;
@@ -7,10 +7,8 @@
7
7
  * with the same function, so no surface counts "files" its own way.
8
8
  */
9
9
 
10
- import type {
11
- SourceSyncCandidateItem,
12
- SourceSyncCandidateTotals,
13
- } from "./schemas";
10
+ import type { SourceSyncCandidateItem } from "./candidate-item";
11
+ import type { SourceSyncCandidateTotals } from "./schemas";
14
12
 
15
13
  /** The relevance facts the count reads (control ADR source-discovery-relevance-funnel). */
16
14
  type RelevanceFacts = "visibility" | "machineState";
@@ -29,6 +29,8 @@ export type {
29
29
  SourceSyncDocDisposition,
30
30
  SourceSyncScopeApprovalState,
31
31
  SourceSyncScopeApproverKind,
32
+ DocumentType,
33
+ KnownDocumentType,
32
34
  } from "./vocabulary";
33
35
 
34
36
  export {
@@ -42,6 +44,7 @@ export {
42
44
  SOURCE_SYNC_DOC_DISPOSITIONS,
43
45
  SOURCE_SYNC_SCOPE_APPROVAL_STATES,
44
46
  SOURCE_SYNC_SCOPE_APPROVER_KINDS,
47
+ DOCUMENT_TYPES,
45
48
  } from "./vocabulary";
46
49
 
47
50
  export type {
@@ -84,11 +87,19 @@ export {
84
87
  } from "./relevance";
85
88
 
86
89
  export type {
87
- SourceSyncDiscoveryRunResponse,
88
90
  SourceSyncCandidateItem,
91
+ SourceSyncCandidateItemPatch,
92
+ } from "./candidate-item";
93
+
94
+ export {
95
+ SourceSyncCandidateItemSchema,
96
+ SourceSyncCandidateItemPatchSchema,
97
+ } from "./candidate-item";
98
+
99
+ export type {
100
+ SourceSyncDiscoveryRunResponse,
89
101
  SourceSyncCandidateSetResponse,
90
102
  SourceSyncCandidateTotals,
91
- SourceSyncCandidateItemPatch,
92
103
  SourceSyncAddCandidateRequest,
93
104
  SourceSyncApproveRequest,
94
105
  SourceSyncScopeApproval,
@@ -107,10 +118,8 @@ export type {
107
118
  export {
108
119
  SOURCE_SYNC_RISK_REASONS,
109
120
  SourceSyncDiscoveryRunResponseSchema,
110
- SourceSyncCandidateItemSchema,
111
121
  SourceSyncCandidateSetResponseSchema,
112
122
  SourceSyncCandidateTotalsSchema,
113
- SourceSyncCandidateItemPatchSchema,
114
123
  SourceSyncAddCandidateRequestSchema,
115
124
  SourceSyncApproveRequestSchema,
116
125
  SourceSyncScopeApprovalSchema,
@@ -51,7 +51,11 @@ export const SIGNAL_REASON_CODES = [
51
51
  ] as const;
52
52
  export type SignalReasonCode = (typeof SIGNAL_REASON_CODES)[number];
53
53
 
54
- /** What was NOT evaluated, or was explored. Never negative evidence (INV-SOURCESYNC-MISSING-EVIDENCE-IS-NOT-EVIDENCE). */
54
+ /**
55
+ * What was NOT evaluated, or was explored, or could not be decided. Never negative evidence
56
+ * (INV-SOURCESYNC-MISSING-EVIDENCE-IS-NOT-EVIDENCE; INV-SOURCESYNC-NONEXECUTION-IS-NOT-NEGATIVE-EVIDENCE):
57
+ * an `execution_failed_*` tier (timeout, budget, provider, parse) reads as uncertainty, never as irrelevance.
58
+ */
55
59
  export const EVALUATION_REASON_CODES = [
56
60
  "not_evaluated_t1",
57
61
  "not_evaluated_t2",
@@ -59,14 +63,26 @@ export const EVALUATION_REASON_CODES = [
59
63
  "folder_truncated",
60
64
  "explored_t1",
61
65
  "explored_t2",
66
+ "peek_insufficient_evidence",
67
+ "type_disagreement",
68
+ "type_below_confidence",
69
+ "execution_failed_t1",
70
+ "execution_failed_t2",
62
71
  ] as const;
63
72
  export type EvaluationReasonCode = (typeof EVALUATION_REASON_CODES)[number];
64
73
 
65
- /** A model tier's contribution. */
74
+ /**
75
+ * A model tier's contribution. `type_policy` carries the POLICY-APPLIED score of the resolved document
76
+ * type (control ADR source-discovery-observation-vs-policy). The peek verdict is domain-named:
77
+ * strategy evidence present / absent. `peek_confirms` / `peek_contradicts` stay for v1-pinned rows.
78
+ */
66
79
  export const MODEL_REASON_CODES = [
67
80
  "title_path_score",
68
81
  "peek_confirms",
69
82
  "peek_contradicts",
83
+ "type_policy",
84
+ "peek_strategy_evidence_present",
85
+ "peek_strategy_evidence_absent",
70
86
  ] as const;
71
87
  export type ModelReasonCode = (typeof MODEL_REASON_CODES)[number];
72
88
 
@@ -173,12 +189,16 @@ type CandidateRelevanceFacts = {
173
189
  sourceClass: string | null;
174
190
  use: string | null;
175
191
  selectionSource: SelectionSource | null;
192
+ documentType: string | null;
193
+ documentTypeConfidence: number | null;
176
194
  };
177
195
 
178
196
  /**
179
197
  * The candidate item's cross-field rules (control ADR source-discovery-relevance-funnel):
180
198
  * a hidden line is never included; classification decides whether a recommendation and a
181
- * class exist; `selectionSource` is present exactly when a treatment is committed.
199
+ * class exist; `selectionSource` is present exactly when a treatment is committed. A
200
+ * document type exists only on a classified line, and carries a confidence exactly when
201
+ * it is a known type (control ADR source-discovery-observation-vs-policy).
182
202
  */
183
203
  export function refineCandidateRelevance(
184
204
  item: CandidateRelevanceFacts,
@@ -213,4 +233,19 @@ export function refineCandidateRelevance(
213
233
  message: "selectionSource is present exactly when use is",
214
234
  });
215
235
  }
236
+ if (item.documentType !== null && unclassified) {
237
+ ctx.addIssue({
238
+ code: "custom",
239
+ message: "an unclassified line has no document type",
240
+ });
241
+ }
242
+ const knownType =
243
+ item.documentType !== null && item.documentType !== "unknown";
244
+ if (knownType !== (item.documentTypeConfidence !== null)) {
245
+ ctx.addIssue({
246
+ code: "custom",
247
+ message:
248
+ "a known document type carries a confidence; unknown and absent carry none",
249
+ });
250
+ }
216
251
  }
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * The Source Sync discovery and candidate-set wire schemas.
3
3
  *
4
- * A candidate item is ONE source proposed for ONE target document, because
5
- * treatment and landing are source x target (INV-SOURCESYNC-LANDING-PER-TARGET).
4
+ * A candidate item (./candidate-item) is ONE source proposed for ONE target
5
+ * document, because treatment and landing are source x target
6
+ * (INV-SOURCESYNC-LANDING-PER-TARGET).
6
7
  * The wire carries facts ABOUT a source and a COUNT of people it is shared with,
7
8
  * never its content and never their addresses: the external provider remains the
8
9
  * ACL authority for the source itself.
@@ -20,28 +21,14 @@ import { z } from "zod";
20
21
  import { DecisionQuestionPresentationSchema } from "../decisions/presentation";
21
22
  import { IsoDateTime } from "../api/primitives";
22
23
  import {
23
- AUDIENCE_FITS,
24
- SHARING_BREADTHS,
25
24
  SOURCE_CLASSES,
26
- SOURCE_SENSITIVITY_CLASSES,
27
25
  SOURCE_SYNC_DOC_DISPOSITIONS,
28
26
  SOURCE_SYNC_DOC_RISKS,
29
27
  SOURCE_SYNC_SCOPE_APPROVAL_STATES,
30
28
  SOURCE_SYNC_SCOPE_APPROVER_KINDS,
31
- SOURCE_USES,
32
29
  } from "./vocabulary";
33
- import {
34
- CANDIDATE_MACHINE_STATES,
35
- CandidateAttentionSchema,
36
- CandidateDiscoverySummarySchema,
37
- CLASSIFICATION_STATES,
38
- PERSON_HOLD_REASONS,
39
- refineCandidateRelevance,
40
- RelevanceReasonSchema,
41
- SELECTION_SOURCES,
42
- SOURCE_RELEVANCE_TIERS,
43
- TARGET_AUDIENCES,
44
- } from "./relevance";
30
+ import { CandidateDiscoverySummarySchema } from "./relevance";
31
+ import { SourceSyncCandidateItemSchema } from "./candidate-item";
45
32
 
46
33
  /** Integer micro-USD as a string — the billing domain's money convention on the wire. */
47
34
  const MicroUsd = z.string().regex(/^[0-9]+$/);
@@ -88,88 +75,6 @@ export const SourceSyncDiscoveryRunResponseSchema = z.object({
88
75
  workflowType: z.literal("source-sync/discover"),
89
76
  });
90
77
 
91
- /** ONE source proposed for ONE target document. */
92
- export const SourceSyncCandidateItemSchema = z
93
- .object({
94
- itemId: z.string().uuid(),
95
- sourceId: z.string().uuid(),
96
- /**
97
- * Display facts about the source. Never its content. When `withheld`, `name` is
98
- * product copy naming only the source's class and period.
99
- */
100
- name: z.string().min(1).max(500),
101
- mimeType: z.string().min(1).max(255),
102
- /** Opens the source IN THE PROVIDER, which remains the ACL authority for it. */
103
- webViewLink: z.string().url().nullable(),
104
- ownerLabel: z.string().max(255).nullable(),
105
- modifiedTime: IsoDateTime.nullable(),
106
- /**
107
- * True when the caller may not open this source: its identifying fields are
108
- * withheld (`webViewLink`, `ownerLabel` and `sharedWithCount` are null, and
109
- * `name` and `matchReason` are product copy). What the approver decides stays.
110
- */
111
- withheld: z.boolean(),
112
- /** 'company' or 'unit:<uuid>'. */
113
- scopeKey: z.string().min(1).max(64),
114
- targetDocId: z.string().uuid(),
115
- targetDocTitle: z.string().min(1).max(255),
116
- /** The funnel's verdict for the wire: `dropped` never reaches it. Hidden = behind the drawer, undecided. */
117
- visibility: z.enum(SOURCE_RELEVANCE_TIERS).exclude(["dropped"]),
118
- /** Separate from visibility: a shown line may still await classification (machineState pending_classification). */
119
- classificationState: z.enum(CLASSIFICATION_STATES),
120
- /** NULL until a classifier has run for this line. */
121
- sourceClass: z.enum(SOURCE_CLASSES).nullable(),
122
- periodLabel: z.string().max(64).nullable(),
123
- seriesRole: z.enum(["current", "history"]).nullable(),
124
- /** The FUSED relevance score (control ADR source-discovery-relevance-funnel), 0..1. */
125
- relevance: z.number().min(0).max(1),
126
- sensitivityClass: z.enum(SOURCE_SENSITIVITY_CLASSES),
127
- sharingBreadth: z.enum(SHARING_BREADTHS),
128
- /** A COUNT of people the provider shares it with, when observed. Never addresses. */
129
- sharedWithCount: z.number().int().min(0).nullable(),
130
- /**
131
- * Whether attaching under THIS target would show the source to more people than the provider does.
132
- * A REQUIRED key with a NULLABLE value. NULL = not computed yet: discovery writes it null and the
133
- * server computes it per source x target when it builds the draft set item. Every consumer reads
134
- * null as UNRESOLVED (fail closed) — never as 'fits'.
135
- */
136
- audienceFit: z.enum(AUDIENCE_FITS).nullable(),
137
- /** Machine state: the sensitivity default. NULL until classified — never `excluded` as a stand-in. */
138
- recommendedUse: z.enum(SOURCE_USES).nullable(),
139
- /** The COMMITTED treatment: null until a person (or an accepted approval default) commits one. */
140
- use: z.enum(SOURCE_USES).nullable(),
141
- /** Provenance of `use`; null exactly when `use` is null (INV-SOURCESYNC-RECOMMENDATION-IS-NOT-COMMITMENT). */
142
- selectionSource: z.enum(SELECTION_SOURCES).nullable(),
143
- /** Field-level write barriers a person placed on this line (INV-SOURCESYNC-HOLDS-ARE-FIELD-BARRIERS). */
144
- personHoldReasons: z.array(z.enum(PERSON_HOLD_REASONS)).max(4),
145
- /** The line's machine lifecycle (INV-SOURCESYNC-SET-IS-RECONCILED). */
146
- machineState: z.enum(CANDIDATE_MACHINE_STATES),
147
- /** Structured reasons for `relevance`; `matchReason` is rendered from these by the server. */
148
- relevanceReasons: z.array(RelevanceReasonSchema).max(8),
149
- /** The audience Attach would disclose to. NULL = unknown — reads as MAY WIDEN (INV-SOURCESYNC-ATTACH-SHOWS-ITS-AUDIENCE). */
150
- targetAudience: z.enum(TARGET_AUDIENCES).nullable(),
151
- /** Which lane earned each model tier's attention (INV-SOURCESYNC-EXPLORATION-RESERVED). */
152
- attention: CandidateAttentionSchema,
153
- /**
154
- * Whether this line is in the set that approval would land — the SERVER's
155
- * answer, never derived from `use`. A decoy, a series' history or a line a
156
- * person left out is false while its `use` is still null.
157
- */
158
- included: z.boolean(),
159
- /** True while a `widens` item — or one whose fit is not computed yet — has no person-chosen use: approval is refused until none remain. */
160
- treatmentUnresolved: z.boolean(),
161
- /**
162
- * Product-owned copy saying why this file was proposed, rendered by the server from
163
- * `relevanceReasons`. Never model-authored prose. Deprecated: removed next release.
164
- */
165
- matchReason: z.string().min(1).max(500),
166
- /** Non-null when the source could not be read (a scanned PDF, an unsupported kind). */
167
- unreadableReason: z
168
- .enum(["unsupported_type", "needs_ocr", "unsupported_kind"])
169
- .nullable(),
170
- })
171
- .superRefine(refineCandidateRelevance);
172
-
173
78
  /** Distinct-file totals over every line of a candidate set the caller may see. */
174
79
  export const SourceSyncCandidateTotalsSchema = z.object({
175
80
  /** Files in the review: at least one line, included or left out. */
@@ -216,27 +121,6 @@ export const SourceSyncCandidateSetResponseSchema = z.object({
216
121
  discovery: z.nullable(CandidateDiscoverySummarySchema),
217
122
  });
218
123
 
219
- /**
220
- * One edit to one candidate item: a PATCH names EXACTLY ONE thing that changes, so bringing
221
- * a line into review never also commits a treatment.
222
- * The SERVER bumps the set's revision on every accepted edit; the client never sends one here.
223
- */
224
- export const SourceSyncCandidateItemPatchSchema = z
225
- .object({
226
- /** Bring into review: hidden → shown. Changes VISIBILITY ONLY (INV-SOURCESYNC-RECOMMENDATION-IS-NOT-COMMITMENT). */
227
- visibility: z.literal("shown").optional(),
228
- /** Attach, direction only, or leave out. Choosing one on a `widens` item RESOLVES its treatment. */
229
- use: z.enum(SOURCE_USES).optional(),
230
- /** Correct the class the classifier chose. */
231
- sourceClass: z.enum(SOURCE_CLASSES).optional(),
232
- /** Re-route to another target document. The server re-computes audienceFit for the new target. */
233
- targetDocId: z.string().uuid().optional(),
234
- })
235
- .strict()
236
- .refine((patch) => Object.keys(patch).length === 1, {
237
- message: "a patch changes exactly one thing",
238
- });
239
-
240
124
  /** "Add one I missed": a file the person picked in the provider's own search. */
241
125
  export const SourceSyncAddCandidateRequestSchema = z
242
126
  .object({
@@ -440,18 +324,12 @@ export const SourceSyncCurrentSynthesisResponseSchema = z.object({
440
324
  export type SourceSyncDiscoveryRunResponse = z.infer<
441
325
  typeof SourceSyncDiscoveryRunResponseSchema
442
326
  >;
443
- export type SourceSyncCandidateItem = z.infer<
444
- typeof SourceSyncCandidateItemSchema
445
- >;
446
327
  export type SourceSyncCandidateSetResponse = z.infer<
447
328
  typeof SourceSyncCandidateSetResponseSchema
448
329
  >;
449
330
  export type SourceSyncCandidateTotals = z.infer<
450
331
  typeof SourceSyncCandidateTotalsSchema
451
332
  >;
452
- export type SourceSyncCandidateItemPatch = z.infer<
453
- typeof SourceSyncCandidateItemPatchSchema
454
- >;
455
333
  export type SourceSyncAddCandidateRequest = z.infer<
456
334
  typeof SourceSyncAddCandidateRequestSchema
457
335
  >;
@@ -110,3 +110,48 @@ export const SOURCE_SYNC_SCOPE_APPROVER_KINDS = [
110
110
  ] as const;
111
111
  export type SourceSyncScopeApproverKind =
112
112
  (typeof SOURCE_SYNC_SCOPE_APPROVER_KINDS)[number];
113
+
114
+ /**
115
+ * What a source IS — a STRUCTURAL name the model observes, never a relevance verdict
116
+ * (control ADR source-discovery-observation-vs-policy). Product policy in the backend maps a
117
+ * resolved type to a family, a source class, a sensitivity and a relevance policy; none of
118
+ * that travels on the wire. A medium is not a meaning: `image_or_screenshot` is graded on
119
+ * content. `unknown` is a real answer (the model could not tell, or two reads disagreed).
120
+ * Five strings are shared with other sets on purpose: board_deck, board_minutes,
121
+ * investor_update, roadmap (SOURCE_CLASSES) and unknown (SHARING_BREADTHS) — same word,
122
+ * separate axis.
123
+ */
124
+ export const DOCUMENT_TYPES = [
125
+ "board_deck",
126
+ "board_minutes",
127
+ "strategy_memo",
128
+ "planning_doc",
129
+ "okr_doc",
130
+ "roadmap",
131
+ "operating_review",
132
+ "all_hands_deck",
133
+ "offsite_notes",
134
+ "investor_update",
135
+ "pitch_deck",
136
+ "product_spec",
137
+ "research_analysis",
138
+ "general_meeting_notes",
139
+ "financial_plan",
140
+ "financial_actuals",
141
+ "equity_admin_record",
142
+ "corporate_formation_record",
143
+ "corporate_governance_record",
144
+ "government_filing",
145
+ "equity_agreement",
146
+ "commercial_agreement",
147
+ "employment_agreement",
148
+ "transaction_record",
149
+ "personal_document",
150
+ "social_event_material",
151
+ "coursework",
152
+ "empty_template",
153
+ "image_or_screenshot",
154
+ "unknown",
155
+ ] as const;
156
+ export type DocumentType = (typeof DOCUMENT_TYPES)[number];
157
+ export type KnownDocumentType = Exclude<DocumentType, "unknown">;
@@ -32,7 +32,23 @@ so every producer, validator and renderer agrees on it.
32
32
  - **Source Sync registers two flows.** `source-sync/discover` (Stage 1)
33
33
  retains nothing; `source-sync/synthesize` (Stage 4) runs only after a person
34
34
  approved the exact set it reads.
35
+ - **Every type has a typed outcome** (`outcome.ts`, control ADR-CTRL-540). A
36
+ terminal transition is classified once, by the backend, into a per-type
37
+ member of `WorkflowOutcome`: a closed `reason`, typed count-only `facts`, a
38
+ `kind` that is a function of the reason (`*_OUTCOME_KIND_OF_REASON`), and an
39
+ explicit `disposition` (`review` or `narrate`). Adding a workflow type means
40
+ adding its outcome member; the union must cover every registered type.
41
+ - **`unknown` is a safety valve, not a status.** It names an unrecognized
42
+ terminal transition, never a workflow whose state could not be read.
43
+
44
+ ## Public API
45
+
46
+ - `ANALYSIS_WORKFLOW_TYPES`, `WorkflowType`, `isWorkflowType` (`types.ts`).
47
+ - `WorkflowOutcomeSchema` / `WorkflowOutcome` and the per-type member schemas,
48
+ reason tables, fact schemas and `WorkflowOutcomeDispositionSchema`
49
+ (`outcome.ts`).
35
50
 
36
51
  ## Dependencies
37
52
 
38
- None. Pure vocabulary data and one pure guard; no `zod`, no runtime imports.
53
+ `zod` (outcome schemas) and `../input-requests/types` (the review
54
+ disposition's request type). Otherwise pure vocabulary data and one guard.
@@ -13,6 +13,12 @@ wire value against it.
13
13
  rejections: a bare `hris-connect` without its namespace, the namespace on
14
14
  its own, a trailing separator, an unknown flow, and non-string inputs.
15
15
 
16
+ - `outcome.test.ts` — the outcome union covers every registered workflow type
17
+ (compile-time), each reason table's kinds are valid, the schema accepts a
18
+ well-formed member and refuses a kind that does not match its reason, a
19
+ review disposition on a non-produced outcome, a reason from another type,
20
+ and extra keys (the facts stay closed).
21
+
16
22
  ## Invariants
17
23
 
18
24
  - These assert VOCABULARY and SHAPE, never behavior. What a workflow of a