@company-semantics/contracts 83.2.0 → 85.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.
@@ -11,11 +11,19 @@
11
11
  * - The wire carries a COUNT of people a source is shared with, never addresses.
12
12
  * - Money is integer micro-USD strings; a decimal or a JSON number is refused.
13
13
  * - A candidate set carries presented questions, never decision effects.
14
+ * - Relevance and treatment are two axes (control ADR source-discovery-relevance-funnel):
15
+ * a hidden line is never included; an unclassified line has no recommendation and
16
+ * no class; `selectionSource` is present exactly when `use` is.
17
+ * - A relevance reason's code belongs to its kind's closed set.
18
+ * - A candidate patch changes exactly one thing: bringing a line into review never
19
+ * also commits a treatment, and a patch can never hide a line.
20
+ * - The set response's discovery block is required and null before any completed run.
14
21
  */
15
22
 
16
23
  import { describe, expect, it } from "vitest";
17
24
  import {
18
- candidateTotalsOf,
25
+ RelevanceReasonSchema,
26
+ SourceSyncCandidateItemPatchSchema,
19
27
  SourceSyncCandidateItemSchema,
20
28
  SourceSyncCandidateSetResponseSchema,
21
29
  SourceSyncDiscoveryRunResponseSchema,
@@ -41,6 +49,8 @@ function candidateItem(overrides: Record<string, unknown> = {}) {
41
49
  scopeKey: "company",
42
50
  targetDocId: TARGET_DOC_ID,
43
51
  targetDocTitle: "Company overview",
52
+ visibility: "shown",
53
+ classificationState: "classified",
44
54
  sourceClass: "board_deck",
45
55
  periodLabel: "Q3 2026",
46
56
  seriesRole: "current",
@@ -51,6 +61,12 @@ function candidateItem(overrides: Record<string, unknown> = {}) {
51
61
  audienceFit: "fits",
52
62
  recommendedUse: "direction_only",
53
63
  use: null,
64
+ selectionSource: null,
65
+ personHoldReasons: [],
66
+ machineState: "fresh",
67
+ relevanceReasons: [],
68
+ targetAudience: "org_wide",
69
+ attention: { t1: null, t2: null },
54
70
  included: true,
55
71
  treatmentUnresolved: false,
56
72
  matchReason: "A board deck for the current quarter.",
@@ -98,13 +114,29 @@ function candidateSet(overrides: Record<string, unknown> = {}) {
98
114
  items: [candidateItem()],
99
115
  questions: [presentedQuestion()],
100
116
  withheldQuestionCount: 0,
101
- totals: { files: 1, filesIncluded: 1, filesNeedingChoice: 0 },
117
+ totals: {
118
+ files: 1,
119
+ filesIncluded: 1,
120
+ filesNeedingChoice: 0,
121
+ filesHidden: 0,
122
+ filesNotRediscovered: 0,
123
+ filesPendingClassification: 0,
124
+ },
102
125
  scopes: [],
103
126
  estimate: {
104
127
  lowMicroUsd: "250000",
105
128
  highMicroUsd: "1250000",
106
129
  capMicroUsd: "2000000",
107
130
  },
131
+ discovery: {
132
+ foundThisRun: 12,
133
+ retainedDecisions: 3,
134
+ notRediscovered: 1,
135
+ pendingClassification: 2,
136
+ filesDropped: 240,
137
+ coveragePercent: 87,
138
+ scanOutcome: "complete",
139
+ },
108
140
  ...overrides,
109
141
  };
110
142
  }
@@ -260,6 +292,208 @@ describe("SourceSyncCandidateSetResponseSchema", () => {
260
292
  });
261
293
  });
262
294
 
295
+ describe("candidate relevance", () => {
296
+ it("a hidden unclassified item has no recommendation, no class and is not included", () => {
297
+ const parsed = SourceSyncCandidateItemSchema.safeParse(
298
+ candidateItem({
299
+ visibility: "hidden",
300
+ classificationState: "unclassified",
301
+ recommendedUse: null,
302
+ sourceClass: null,
303
+ included: false,
304
+ targetAudience: null,
305
+ machineState: "pending_classification",
306
+ }),
307
+ );
308
+ expect(parsed.success).toBe(true);
309
+ expect(parsed.data?.recommendedUse).toBeNull();
310
+ expect(parsed.data?.sourceClass).toBeNull();
311
+ for (const claim of [
312
+ { recommendedUse: "attached" },
313
+ { sourceClass: "board_deck" },
314
+ ]) {
315
+ expect(
316
+ SourceSyncCandidateItemSchema.safeParse(
317
+ candidateItem({
318
+ visibility: "hidden",
319
+ classificationState: "unclassified",
320
+ recommendedUse: null,
321
+ sourceClass: null,
322
+ included: false,
323
+ ...claim,
324
+ }),
325
+ ).success,
326
+ ).toBe(false);
327
+ }
328
+ });
329
+
330
+ it("a classified item has a recommendation and a class", () => {
331
+ for (const missing of [{ recommendedUse: null }, { sourceClass: null }]) {
332
+ expect(
333
+ SourceSyncCandidateItemSchema.safeParse(candidateItem(missing)).success,
334
+ ).toBe(false);
335
+ }
336
+ });
337
+
338
+ it("a shown item carries a reason of every kind", () => {
339
+ const relevanceReasons = [
340
+ { kind: "lexicon", code: "title_term", term: "board", count: 2 },
341
+ { kind: "signal", code: "version_churn", value: 41 },
342
+ { kind: "signal", code: "owner_executive", value: null },
343
+ { kind: "evaluation", code: "not_evaluated_t2" },
344
+ { kind: "model", code: "title_path_score", score: 0.81 },
345
+ { kind: "model", code: "peek_confirms", score: null },
346
+ ];
347
+ const parsed = SourceSyncCandidateItemSchema.safeParse(
348
+ candidateItem({
349
+ relevanceReasons,
350
+ attention: { t1: "exploit", t2: "explore" },
351
+ personHoldReasons: ["relevance_override"],
352
+ }),
353
+ );
354
+ expect(parsed.success).toBe(true);
355
+ expect(parsed.data?.relevanceReasons.map((reason) => reason.kind)).toEqual([
356
+ "lexicon",
357
+ "signal",
358
+ "signal",
359
+ "evaluation",
360
+ "model",
361
+ "model",
362
+ ]);
363
+ });
364
+
365
+ it("refuses more than eight reasons", () => {
366
+ const reason = { kind: "evaluation", code: "explored_t1" };
367
+ expect(
368
+ SourceSyncCandidateItemSchema.safeParse(
369
+ candidateItem({ relevanceReasons: Array(9).fill(reason) }),
370
+ ).success,
371
+ ).toBe(false);
372
+ });
373
+
374
+ it("a reason whose code belongs to another kind is refused", () => {
375
+ for (const reason of [
376
+ { kind: "lexicon", code: "version_churn", term: "board", count: 1 },
377
+ { kind: "signal", code: "title_term", value: 1 },
378
+ { kind: "evaluation", code: "peek_confirms" },
379
+ { kind: "model", code: "folder_unresolved", score: 0.5 },
380
+ ]) {
381
+ expect(RelevanceReasonSchema.safeParse(reason).success).toBe(false);
382
+ }
383
+ });
384
+
385
+ it("refuses a lexicon reason without a term or a count, and a model score outside 0..1", () => {
386
+ for (const reason of [
387
+ { kind: "lexicon", code: "title_term", count: 1 },
388
+ { kind: "lexicon", code: "title_term", term: "", count: 1 },
389
+ { kind: "lexicon", code: "title_term", term: "board", count: 0 },
390
+ { kind: "model", code: "title_path_score", score: 1.2 },
391
+ ]) {
392
+ expect(RelevanceReasonSchema.safeParse(reason).success).toBe(false);
393
+ }
394
+ });
395
+
396
+ it("a hidden item that claims inclusion is refused", () => {
397
+ const parsed = SourceSyncCandidateItemSchema.safeParse(
398
+ candidateItem({ visibility: "hidden", included: true }),
399
+ );
400
+ expect(parsed.success).toBe(false);
401
+ expect(parsed.error?.issues[0]?.message).toBe(
402
+ "a hidden line is never included",
403
+ );
404
+ });
405
+
406
+ it("refuses the dropped tier on the wire item", () => {
407
+ expect(
408
+ SourceSyncCandidateItemSchema.safeParse(
409
+ candidateItem({ visibility: "dropped", included: false }),
410
+ ).success,
411
+ ).toBe(false);
412
+ });
413
+
414
+ it("carries a selection source exactly when a treatment is committed", () => {
415
+ const committed = { use: "attached", treatmentUnresolved: false };
416
+ expect(
417
+ SourceSyncCandidateItemSchema.safeParse(
418
+ candidateItem({ ...committed, selectionSource: "person" }),
419
+ ).success,
420
+ ).toBe(true);
421
+ expect(
422
+ SourceSyncCandidateItemSchema.safeParse(
423
+ candidateItem({ ...committed, selectionSource: "approval_default" }),
424
+ ).success,
425
+ ).toBe(true);
426
+ expect(
427
+ SourceSyncCandidateItemSchema.safeParse(
428
+ candidateItem({ ...committed, selectionSource: null }),
429
+ ).success,
430
+ ).toBe(false);
431
+ expect(
432
+ SourceSyncCandidateItemSchema.safeParse(
433
+ candidateItem({ use: null, selectionSource: "person" }),
434
+ ).success,
435
+ ).toBe(false);
436
+ });
437
+ });
438
+
439
+ describe("SourceSyncCandidateItemPatchSchema", () => {
440
+ it("bring into review is a patch of visibility alone", () => {
441
+ expect(
442
+ SourceSyncCandidateItemPatchSchema.safeParse({ visibility: "shown" })
443
+ .success,
444
+ ).toBe(true);
445
+ expect(
446
+ SourceSyncCandidateItemPatchSchema.safeParse({ visibility: "hidden" })
447
+ .success,
448
+ ).toBe(false);
449
+ });
450
+
451
+ it("a patch may not both promote and choose a treatment", () => {
452
+ for (const patch of [
453
+ { visibility: "shown", use: "attached" },
454
+ { visibility: "shown", targetDocId: TARGET_DOC_ID },
455
+ { use: "attached", sourceClass: "board_deck" },
456
+ {},
457
+ ]) {
458
+ expect(SourceSyncCandidateItemPatchSchema.safeParse(patch).success).toBe(
459
+ false,
460
+ );
461
+ }
462
+ });
463
+ });
464
+
465
+ describe("SourceSyncCandidateSetResponseSchema discovery", () => {
466
+ it("a set response before any completed run has a null discovery block", () => {
467
+ expect(
468
+ SourceSyncCandidateSetResponseSchema.safeParse(
469
+ candidateSet({ discovery: null }),
470
+ ).success,
471
+ ).toBe(true);
472
+ const { discovery: _omitted, ...withoutKey } = candidateSet();
473
+ expect(
474
+ SourceSyncCandidateSetResponseSchema.safeParse(withoutKey).success,
475
+ ).toBe(false);
476
+ });
477
+
478
+ it("accepts a discovery block and refuses a coverage above 100 or an unknown outcome", () => {
479
+ const set = candidateSet();
480
+ expect(SourceSyncCandidateSetResponseSchema.safeParse(set).success).toBe(
481
+ true,
482
+ );
483
+ for (const change of [
484
+ { coveragePercent: 101 },
485
+ { scanOutcome: "timed_out" },
486
+ { filesDropped: -1 },
487
+ ]) {
488
+ expect(
489
+ SourceSyncCandidateSetResponseSchema.safeParse(
490
+ candidateSet({ discovery: { ...set.discovery, ...change } }),
491
+ ).success,
492
+ ).toBe(false);
493
+ }
494
+ });
495
+ });
496
+
263
497
  describe("SourceSyncDiscoveryRunResponseSchema", () => {
264
498
  it("refuses a workflow type other than source-sync/discover", () => {
265
499
  expect(
@@ -281,16 +515,3 @@ describe("SourceSyncDiscoveryRunResponseSchema", () => {
281
515
  ).toBe(true);
282
516
  });
283
517
  });
284
-
285
- describe("candidateTotalsOf", () => {
286
- it("counts distinct files, never lines", () => {
287
- expect(
288
- candidateTotalsOf([
289
- { sourceId: "a", included: true, treatmentUnresolved: true },
290
- { sourceId: "a", included: true, treatmentUnresolved: false },
291
- { sourceId: "b", included: false, treatmentUnresolved: true },
292
- { sourceId: "c", included: true, treatmentUnresolved: false },
293
- ]),
294
- ).toEqual({ files: 3, filesIncluded: 2, filesNeedingChoice: 1 });
295
- });
296
- });
@@ -6,13 +6,31 @@
6
6
  * order.
7
7
  *
8
8
  * INVARIANTS TESTED:
9
- * - Each of the ten closed sets has exactly the members this release fixes
9
+ * - Each of the twenty-three closed sets (ten Source Sync, thirteen discovery
10
+ * relevance) has exactly the members this release fixes
10
11
  * - `decoy` is a source class; `unknown` is a sharing breadth
11
12
  * - No member string appears in two sets
13
+ * - The four relevance reason code sets are pairwise disjoint
12
14
  */
13
15
 
14
16
  import { describe, it, expect } from "vitest";
15
17
 
18
+ import {
19
+ ATTENTION_LANES,
20
+ CANDIDATE_MACHINE_STATES,
21
+ CLASSIFICATION_STATES,
22
+ EVALUATION_REASON_CODES,
23
+ LEXICON_REASON_CODES,
24
+ MODEL_REASON_CODES,
25
+ PERSON_HOLD_REASONS,
26
+ RELEVANCE_REASON_KINDS,
27
+ SCAN_OUTCOMES,
28
+ SELECTION_SOURCES,
29
+ SIGNAL_REASON_CODES,
30
+ SOURCE_RELEVANCE_TIERS,
31
+ TARGET_AUDIENCES,
32
+ } from "../relevance";
33
+
16
34
  import {
17
35
  AUDIENCE_FITS,
18
36
  SHARING_BREADTHS,
@@ -104,6 +122,108 @@ const VOCABULARIES = [
104
122
  tuple: SOURCE_SYNC_SCOPE_APPROVER_KINDS,
105
123
  members: ["org_admin", "unit_head"],
106
124
  },
125
+ {
126
+ name: "SOURCE_RELEVANCE_TIERS",
127
+ tuple: SOURCE_RELEVANCE_TIERS,
128
+ members: ["dropped", "hidden", "shown"],
129
+ },
130
+ {
131
+ name: "CLASSIFICATION_STATES",
132
+ tuple: CLASSIFICATION_STATES,
133
+ members: ["unclassified", "classified"],
134
+ },
135
+ {
136
+ name: "RELEVANCE_REASON_KINDS",
137
+ tuple: RELEVANCE_REASON_KINDS,
138
+ members: ["lexicon", "signal", "evaluation", "model"],
139
+ },
140
+ {
141
+ name: "LEXICON_REASON_CODES",
142
+ tuple: LEXICON_REASON_CODES,
143
+ members: [
144
+ "title_term",
145
+ "folder_term",
146
+ "negative_title_term",
147
+ "negative_folder_term",
148
+ ],
149
+ },
150
+ {
151
+ name: "SIGNAL_REASON_CODES",
152
+ tuple: SIGNAL_REASON_CODES,
153
+ members: [
154
+ "folder_probe_hit",
155
+ "title_probe_hit",
156
+ "version_churn",
157
+ "mime_class",
158
+ "size_band",
159
+ "dated_series",
160
+ "recent_edit",
161
+ "owner_unit_head",
162
+ "owner_executive",
163
+ "sharing_breadth",
164
+ "duplicate_title",
165
+ "personal_mime",
166
+ ],
167
+ },
168
+ {
169
+ name: "EVALUATION_REASON_CODES",
170
+ tuple: EVALUATION_REASON_CODES,
171
+ members: [
172
+ "not_evaluated_t1",
173
+ "not_evaluated_t2",
174
+ "folder_unresolved",
175
+ "folder_truncated",
176
+ "explored_t1",
177
+ "explored_t2",
178
+ ],
179
+ },
180
+ {
181
+ name: "MODEL_REASON_CODES",
182
+ tuple: MODEL_REASON_CODES,
183
+ members: ["title_path_score", "peek_confirms", "peek_contradicts"],
184
+ },
185
+ {
186
+ name: "TARGET_AUDIENCES",
187
+ tuple: TARGET_AUDIENCES,
188
+ members: ["restricted", "unit", "org_wide"],
189
+ },
190
+ {
191
+ name: "ATTENTION_LANES",
192
+ tuple: ATTENTION_LANES,
193
+ members: ["exploit", "explore"],
194
+ },
195
+ {
196
+ name: "CANDIDATE_MACHINE_STATES",
197
+ tuple: CANDIDATE_MACHINE_STATES,
198
+ members: [
199
+ "fresh",
200
+ "refreshed",
201
+ "filtered_out",
202
+ "not_rediscovered",
203
+ "pending_classification",
204
+ "retired",
205
+ ],
206
+ },
207
+ {
208
+ name: "SELECTION_SOURCES",
209
+ tuple: SELECTION_SOURCES,
210
+ members: ["person", "approval_default"],
211
+ },
212
+ {
213
+ name: "PERSON_HOLD_REASONS",
214
+ tuple: PERSON_HOLD_REASONS,
215
+ members: [
216
+ "relevance_override",
217
+ "treatment_selection",
218
+ "target_override",
219
+ "manual_add",
220
+ ],
221
+ },
222
+ {
223
+ name: "SCAN_OUTCOMES",
224
+ tuple: SCAN_OUTCOMES,
225
+ members: ["complete", "budget_exhausted", "file_ceiling", "stopped"],
226
+ },
107
227
  ] as const satisfies readonly {
108
228
  name: string;
109
229
  tuple: readonly string[];
@@ -142,4 +262,18 @@ describe("Source Sync closed sets", () => {
142
262
  const all = VOCABULARIES.flatMap(({ tuple }) => [...tuple]);
143
263
  expect(new Set(all).size).toBe(all.length);
144
264
  });
265
+
266
+ it("the four reason code sets are pairwise disjoint", () => {
267
+ const codeSets: readonly (readonly string[])[] = [
268
+ LEXICON_REASON_CODES,
269
+ SIGNAL_REASON_CODES,
270
+ EVALUATION_REASON_CODES,
271
+ MODEL_REASON_CODES,
272
+ ];
273
+ codeSets.forEach((a, i) => {
274
+ codeSets.slice(i + 1).forEach((b) => {
275
+ expect(a.filter((code) => b.includes(code))).toEqual([]);
276
+ });
277
+ });
278
+ });
145
279
  });
@@ -12,15 +12,19 @@ import type {
12
12
  SourceSyncCandidateTotals,
13
13
  } from "./schemas";
14
14
 
15
+ /** The relevance facts the count reads (control ADR source-discovery-relevance-funnel). */
16
+ type RelevanceFacts = "visibility" | "machineState";
17
+
15
18
  /** The item facts the count reads. */
16
19
  export type CandidateTotalsInput = Pick<
17
20
  SourceSyncCandidateItem,
18
- "sourceId" | "included" | "treatmentUnresolved"
21
+ "sourceId" | "included" | "treatmentUnresolved" | RelevanceFacts
19
22
  >;
20
23
 
21
24
  /**
22
25
  * Included when any of the file's lines is; in need of a choice when any
23
- * included line's treatment is unresolved.
26
+ * included line's treatment is unresolved. Hidden only when EVERY line of the
27
+ * file is hidden; not rediscovered / pending classification when ANY line is.
24
28
  */
25
29
  export function candidateTotalsOf(
26
30
  items: readonly CandidateTotalsInput[],
@@ -28,15 +32,29 @@ export function candidateTotalsOf(
28
32
  const files = new Set<string>();
29
33
  const included = new Set<string>();
30
34
  const needingChoice = new Set<string>();
35
+ const anyShown = new Set<string>();
36
+ const notRediscovered = new Set<string>();
37
+ const pendingClassification = new Set<string>();
31
38
  for (const item of items) {
32
39
  files.add(item.sourceId);
40
+ if (item.visibility === "shown") anyShown.add(item.sourceId);
41
+ if (item.machineState === "not_rediscovered") {
42
+ notRediscovered.add(item.sourceId);
43
+ }
44
+ if (item.machineState === "pending_classification") {
45
+ pendingClassification.add(item.sourceId);
46
+ }
33
47
  if (!item.included) continue;
34
48
  included.add(item.sourceId);
35
49
  if (item.treatmentUnresolved) needingChoice.add(item.sourceId);
36
50
  }
51
+ const hidden = [...files].filter((id) => !anyShown.has(id)).length;
37
52
  return {
38
53
  files: files.size,
39
54
  filesIncluded: included.size,
40
55
  filesNeedingChoice: needingChoice.size,
56
+ filesHidden: hidden,
57
+ filesNotRediscovered: notRediscovered.size,
58
+ filesPendingClassification: pendingClassification.size,
41
59
  };
42
60
  }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Re-exports the closed Source Sync vocabularies (the sets the backend persists
5
5
  * and mirrors as database CHECK constraints, and the app handles exhaustively)
6
- * the discovery and candidate-set response schemas, the candidate-edit and
6
+ * including the discovery relevance vocabulary, the discovery and candidate-set response schemas, the candidate-edit and
7
7
  * per-scope approval request and response schemas, the per-document change
8
8
  * plan and apply schemas, the source-resolution rule schemas, the staleness
9
9
  * finding and standing-order schemas, and the Drive-reach status and upgrade
@@ -44,6 +44,45 @@ export {
44
44
  SOURCE_SYNC_SCOPE_APPROVER_KINDS,
45
45
  } from "./vocabulary";
46
46
 
47
+ export type {
48
+ SourceRelevanceTier,
49
+ ClassificationState,
50
+ RelevanceReasonKind,
51
+ LexiconReasonCode,
52
+ SignalReasonCode,
53
+ EvaluationReasonCode,
54
+ ModelReasonCode,
55
+ TargetAudience,
56
+ AttentionLane,
57
+ CandidateMachineState,
58
+ SelectionSource,
59
+ PersonHoldReason,
60
+ ScanOutcome,
61
+ RelevanceReason,
62
+ CandidateAttention,
63
+ CandidateDiscoverySummary,
64
+ } from "./relevance";
65
+
66
+ export {
67
+ SOURCE_RELEVANCE_TIERS,
68
+ CLASSIFICATION_STATES,
69
+ RELEVANCE_REASON_KINDS,
70
+ LEXICON_REASON_CODES,
71
+ SIGNAL_REASON_CODES,
72
+ EVALUATION_REASON_CODES,
73
+ MODEL_REASON_CODES,
74
+ TARGET_AUDIENCES,
75
+ ATTENTION_LANES,
76
+ CANDIDATE_MACHINE_STATES,
77
+ SELECTION_SOURCES,
78
+ PERSON_HOLD_REASONS,
79
+ SCAN_OUTCOMES,
80
+ RelevanceReasonSchema,
81
+ CandidateAttentionSchema,
82
+ CandidateDiscoverySummarySchema,
83
+ refineCandidateRelevance,
84
+ } from "./relevance";
85
+
47
86
  export type {
48
87
  SourceSyncDiscoveryRunResponse,
49
88
  SourceSyncCandidateItem,