@company-semantics/contracts 83.2.0 → 84.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 +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +54 -3
- package/src/source-sync/README.md +59 -1
- package/src/source-sync/__tests__/approval-schemas.test.ts +2 -2
- package/src/source-sync/__tests__/barrel.test.ts +14 -0
- package/src/source-sync/__tests__/candidate-totals.test.ts +119 -0
- package/src/source-sync/__tests__/schemas.test.ts +236 -15
- package/src/source-sync/__tests__/vocabulary.test.ts +135 -1
- package/src/source-sync/candidate-totals.ts +20 -2
- package/src/source-sync/index.ts +40 -1
- package/src/source-sync/relevance.ts +216 -0
- package/src/source-sync/schemas.ts +106 -59
package/package.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
|
|
2
|
-
export const SPEC_HASH = '
|
|
3
|
-
export const SPEC_HASH_FULL = '
|
|
2
|
+
export const SPEC_HASH = '53f462b4c90b' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '53f462b4c90ba419e71d55d82e19bb6a9836d95aeaaae6299b8124ac340320d8' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -8876,7 +8876,10 @@ export interface components {
|
|
|
8876
8876
|
targetDocId: string;
|
|
8877
8877
|
targetDocTitle: string;
|
|
8878
8878
|
/** @enum {string} */
|
|
8879
|
-
|
|
8879
|
+
visibility: "hidden" | "shown";
|
|
8880
|
+
/** @enum {string} */
|
|
8881
|
+
classificationState: "unclassified" | "classified";
|
|
8882
|
+
sourceClass: ("board_deck" | "board_minutes" | "investor_update" | "financial_brief" | "budget_forecast" | "okr_plan" | "annual_plan" | "all_hands" | "unit_strategy" | "roadmap" | "qbr" | "other" | "decoy") | null;
|
|
8880
8883
|
periodLabel: string | null;
|
|
8881
8884
|
seriesRole: ("current" | "history") | null;
|
|
8882
8885
|
relevance: number;
|
|
@@ -8886,9 +8889,42 @@ export interface components {
|
|
|
8886
8889
|
sharingBreadth: "restricted_few" | "group" | "domain" | "anyone" | "unknown";
|
|
8887
8890
|
sharedWithCount: number | null;
|
|
8888
8891
|
audienceFit: ("fits" | "widens") | null;
|
|
8889
|
-
|
|
8890
|
-
recommendedUse: "attached" | "direction_only" | "excluded";
|
|
8892
|
+
recommendedUse: ("attached" | "direction_only" | "excluded") | null;
|
|
8891
8893
|
use: ("attached" | "direction_only" | "excluded") | null;
|
|
8894
|
+
selectionSource: ("person" | "approval_default") | null;
|
|
8895
|
+
personHoldReasons: ("relevance_override" | "treatment_selection" | "target_override" | "manual_add")[];
|
|
8896
|
+
/** @enum {string} */
|
|
8897
|
+
machineState: "fresh" | "refreshed" | "filtered_out" | "not_rediscovered" | "pending_classification" | "retired";
|
|
8898
|
+
relevanceReasons: ({
|
|
8899
|
+
/** @constant */
|
|
8900
|
+
kind: "lexicon";
|
|
8901
|
+
/** @enum {string} */
|
|
8902
|
+
code: "title_term" | "folder_term" | "negative_title_term" | "negative_folder_term";
|
|
8903
|
+
term: string;
|
|
8904
|
+
count: number;
|
|
8905
|
+
} | {
|
|
8906
|
+
/** @constant */
|
|
8907
|
+
kind: "signal";
|
|
8908
|
+
/** @enum {string} */
|
|
8909
|
+
code: "folder_probe_hit" | "title_probe_hit" | "version_churn" | "mime_class" | "size_band" | "dated_series" | "recent_edit" | "owner_unit_head" | "owner_executive" | "sharing_breadth" | "duplicate_title" | "personal_mime";
|
|
8910
|
+
value: number | null;
|
|
8911
|
+
} | {
|
|
8912
|
+
/** @constant */
|
|
8913
|
+
kind: "evaluation";
|
|
8914
|
+
/** @enum {string} */
|
|
8915
|
+
code: "not_evaluated_t1" | "not_evaluated_t2" | "folder_unresolved" | "folder_truncated" | "explored_t1" | "explored_t2";
|
|
8916
|
+
} | {
|
|
8917
|
+
/** @constant */
|
|
8918
|
+
kind: "model";
|
|
8919
|
+
/** @enum {string} */
|
|
8920
|
+
code: "title_path_score" | "peek_confirms" | "peek_contradicts";
|
|
8921
|
+
score: number | null;
|
|
8922
|
+
})[];
|
|
8923
|
+
targetAudience: ("restricted" | "unit" | "org_wide") | null;
|
|
8924
|
+
attention: {
|
|
8925
|
+
t1: ("exploit" | "explore") | null;
|
|
8926
|
+
t2: ("exploit" | "explore") | null;
|
|
8927
|
+
};
|
|
8892
8928
|
included: boolean;
|
|
8893
8929
|
treatmentUnresolved: boolean;
|
|
8894
8930
|
matchReason: string;
|
|
@@ -8931,6 +8967,9 @@ export interface components {
|
|
|
8931
8967
|
files: number;
|
|
8932
8968
|
filesIncluded: number;
|
|
8933
8969
|
filesNeedingChoice: number;
|
|
8970
|
+
filesHidden: number;
|
|
8971
|
+
filesNotRediscovered: number;
|
|
8972
|
+
filesPendingClassification: number;
|
|
8934
8973
|
};
|
|
8935
8974
|
scopes: {
|
|
8936
8975
|
scopeKey: string;
|
|
@@ -8953,6 +8992,16 @@ export interface components {
|
|
|
8953
8992
|
highMicroUsd: string;
|
|
8954
8993
|
capMicroUsd: string;
|
|
8955
8994
|
} | null;
|
|
8995
|
+
discovery: {
|
|
8996
|
+
foundThisRun: number;
|
|
8997
|
+
retainedDecisions: number;
|
|
8998
|
+
notRediscovered: number;
|
|
8999
|
+
pendingClassification: number;
|
|
9000
|
+
filesDropped: number;
|
|
9001
|
+
coveragePercent: number;
|
|
9002
|
+
/** @enum {string} */
|
|
9003
|
+
scanOutcome: "complete" | "budget_exhausted" | "file_ceiling" | "stopped";
|
|
9004
|
+
} | null;
|
|
8956
9005
|
};
|
|
8957
9006
|
SourceSyncApproveResponse: {
|
|
8958
9007
|
/** Format: uuid */
|
|
@@ -17884,6 +17933,8 @@ export interface operations {
|
|
|
17884
17933
|
requestBody: {
|
|
17885
17934
|
content: {
|
|
17886
17935
|
"application/json": {
|
|
17936
|
+
/** @constant */
|
|
17937
|
+
visibility?: "shown";
|
|
17887
17938
|
/** @enum {string} */
|
|
17888
17939
|
use?: "attached" | "direction_only" | "excluded";
|
|
17889
17940
|
/** @enum {string} */
|
|
@@ -107,6 +107,54 @@ way. This directory holds that shared spelling.
|
|
|
107
107
|
domain is the largest trust change in the wave, and an empty body is
|
|
108
108
|
indistinguishable from a mistaken or replayed body-less POST.
|
|
109
109
|
|
|
110
|
+
## Relevance
|
|
111
|
+
|
|
112
|
+
Doctrine: control ADR `source-discovery-relevance-funnel`. Vocabulary: ADR
|
|
113
|
+
`source-discovery-relevance-vocabulary`. The sets live in `relevance.ts`.
|
|
114
|
+
|
|
115
|
+
- **Two axes on the wire.**
|
|
116
|
+
- **Relevance** is the funnel's verdict: `visibility` is `shown` or
|
|
117
|
+
`hidden`.
|
|
118
|
+
- **Treatment** is a person's decision: `recommendedUse` (machine state,
|
|
119
|
+
null until classified), `use` (the committed treatment, null until
|
|
120
|
+
chosen) and `selectionSource` (`person` or `approval_default`, present
|
|
121
|
+
exactly when `use` is).
|
|
122
|
+
|
|
123
|
+
Neither axis changes the other. The disclosure question is asked only of
|
|
124
|
+
lines that cleared relevance.
|
|
125
|
+
|
|
126
|
+
- **Hidden is not dropped.** `SOURCE_RELEVANCE_TIERS` includes `dropped` so the
|
|
127
|
+
backend can mirror it in persistence, but the item schema excludes it: a
|
|
128
|
+
dropped sighting never reaches the wire. A hidden line sits behind the
|
|
129
|
+
drawer, is never `included`, and a person can bring it into review with the
|
|
130
|
+
patch `{ visibility: "shown" }`. The patch changes exactly one field, so
|
|
131
|
+
promoting a line never also commits a treatment, and no client can hide a
|
|
132
|
+
line.
|
|
133
|
+
- **Classification is separate from visibility.** `classificationState` is
|
|
134
|
+
`unclassified` (so `sourceClass` and `recommendedUse` are both null) or
|
|
135
|
+
`classified` (both non-null). A shown line can still be
|
|
136
|
+
`pending_classification` in its `machineState`.
|
|
137
|
+
- **Reasons are a discriminated union.** `relevanceReasons` (≤ 8) is
|
|
138
|
+
`RelevanceReason`: `lexicon` (a product-owned `term` and a `count`),
|
|
139
|
+
`signal` (a `value` or null), `evaluation` (a code only) or `model` (a
|
|
140
|
+
`score` or null). Each kind has its own disjoint code set, and the app
|
|
141
|
+
switches over `kind`, then `code`, exhaustively. `relevance` is the fused
|
|
142
|
+
score, and `matchReason` is server copy rendered from the reasons
|
|
143
|
+
(deprecated).
|
|
144
|
+
- **Holds and lifecycle travel on the item.** `personHoldReasons` are the
|
|
145
|
+
field-level barriers a person's actions placed on the line. `machineState`
|
|
146
|
+
is its lifecycle (`fresh`, `refreshed`, `filtered_out`, `not_rediscovered`,
|
|
147
|
+
`pending_classification`, `retired`). `attention` says which lane
|
|
148
|
+
(`exploit` or `explore`) earned each model tier's look.
|
|
149
|
+
- **Attach shows its audience.** `targetAudience` is the audience Attach would
|
|
150
|
+
disclose to. Null means unknown and reads as "may widen".
|
|
151
|
+
- **No single ambiguous total.** `discovery` on the set response carries the
|
|
152
|
+
last completed run's counts (found, retained decisions, not rediscovered,
|
|
153
|
+
pending classification, dropped, coverage and how the scan ended). It is
|
|
154
|
+
null before any run completes. `totals` adds `filesHidden`,
|
|
155
|
+
`filesNotRediscovered` and `filesPendingClassification`, counted by
|
|
156
|
+
`candidateTotalsOf` over distinct files.
|
|
157
|
+
|
|
110
158
|
## Public API
|
|
111
159
|
|
|
112
160
|
- `SOURCE_CLASSES` / `SourceClass` — what kind of document a source is.
|
|
@@ -135,7 +183,16 @@ way. This directory holds that shared spelling.
|
|
|
135
183
|
`candidateTotalsOf(items)` — THE file count: distinct `sourceId`, never lines
|
|
136
184
|
(ADR-CONTRACTS-195).
|
|
137
185
|
- `SourceSyncCandidateItemPatchSchema` / `SourceSyncCandidateItemPatch` — one
|
|
138
|
-
edit to one candidate item
|
|
186
|
+
edit to one candidate item: exactly one of visibility (`shown` only), use,
|
|
187
|
+
class or target.
|
|
188
|
+
- `SOURCE_RELEVANCE_TIERS`, `CLASSIFICATION_STATES`, `RELEVANCE_REASON_KINDS`,
|
|
189
|
+
`LEXICON_REASON_CODES`, `SIGNAL_REASON_CODES`, `EVALUATION_REASON_CODES`,
|
|
190
|
+
`MODEL_REASON_CODES`, `TARGET_AUDIENCES`, `ATTENTION_LANES`,
|
|
191
|
+
`CANDIDATE_MACHINE_STATES`, `SELECTION_SOURCES`, `PERSON_HOLD_REASONS`,
|
|
192
|
+
`SCAN_OUTCOMES` and their derived unions — the relevance vocabulary (see
|
|
193
|
+
Relevance).
|
|
194
|
+
- `RelevanceReasonSchema` / `RelevanceReason` — why a candidate scored as it
|
|
195
|
+
did: a discriminated union on `kind`.
|
|
139
196
|
- `SourceSyncAddCandidateRequestSchema` / `SourceSyncAddCandidateRequest` —
|
|
140
197
|
"add one I missed": a provider file id picked in the provider's own search.
|
|
141
198
|
- `SourceSyncApproveRequestSchema` / `SourceSyncApproveRequest` — approve the
|
|
@@ -210,4 +267,5 @@ way. This directory holds that shared spelling.
|
|
|
210
267
|
|
|
211
268
|
`rules.ts` imports only `zod`, `../api/primitives` and `./vocabulary`.
|
|
212
269
|
`upkeep.ts` and `reach.ts` import only `zod` and `../api/primitives`.
|
|
270
|
+
`relevance.ts` imports only `zod`.
|
|
213
271
|
`vocabulary.ts` itself has no imports.
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* backend answers with.
|
|
6
6
|
*
|
|
7
7
|
* INVARIANTS TESTED:
|
|
8
|
-
* - A candidate patch changes
|
|
9
|
-
* unknown key is an error, not surplus.
|
|
8
|
+
* - A candidate patch changes exactly one thing: an empty patch is refused, and
|
|
9
|
+
* an unknown key is an error, not surplus.
|
|
10
10
|
* - An approval echoes the revision the person reviewed and carries the spend
|
|
11
11
|
* cap as integer micro-USD; either one missing is refused.
|
|
12
12
|
* - An approval answers PER SCOPE: one row per scope approved, each bound to its
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* below, which fails typecheck if one is missing at the root).
|
|
17
17
|
* - The same two checks hold for the Release D names: staleness findings,
|
|
18
18
|
* standing orders and the Drive-reach shapes.
|
|
19
|
+
* - Every runtime name `relevance.ts` declares is reachable from the root as
|
|
20
|
+
* the same binding.
|
|
19
21
|
* - `ACTION_ITEM_KINDS` at the root carries `sourceSync.approval_pending`.
|
|
20
22
|
* - The root re-exports the domain barrel explicitly.
|
|
21
23
|
*
|
|
@@ -66,6 +68,7 @@ import type {
|
|
|
66
68
|
SourceSyncStandingOrdersResponse,
|
|
67
69
|
} from "../../index";
|
|
68
70
|
import * as reach from "../reach";
|
|
71
|
+
import * as relevance from "../relevance";
|
|
69
72
|
import * as rules from "../rules";
|
|
70
73
|
import * as schemas from "../schemas";
|
|
71
74
|
import * as upkeep from "../upkeep";
|
|
@@ -236,6 +239,17 @@ describe("source-sync Release C package surface", () => {
|
|
|
236
239
|
}
|
|
237
240
|
});
|
|
238
241
|
|
|
242
|
+
it("every discovery relevance set is exported from the package root", () => {
|
|
243
|
+
const root = rootBarrel as Record<string, unknown>;
|
|
244
|
+
const names = Object.keys(relevance);
|
|
245
|
+
expect(names.length).toBeGreaterThan(0);
|
|
246
|
+
for (const name of names) {
|
|
247
|
+
expect(root[name], `${name} at the root`).toBe(
|
|
248
|
+
(relevance as Record<string, unknown>)[name],
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
239
253
|
it("exposes sourceSync.approval_pending in ACTION_ITEM_KINDS at the root", () => {
|
|
240
254
|
expect(rootBarrel.ACTION_ITEM_KINDS).toContain(
|
|
241
255
|
"sourceSync.approval_pending",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one file count for a candidate review.
|
|
3
|
+
*
|
|
4
|
+
* INVARIANTS TESTED:
|
|
5
|
+
* - Every total counts DISTINCT files (`sourceId`), never lines: a file routed to
|
|
6
|
+
* two documents is two lines and one file in every total.
|
|
7
|
+
* - A file is hidden only when EVERY line of it is hidden; one shown line shows it.
|
|
8
|
+
* - A file is not rediscovered / pending classification when ANY line of it is.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
import { candidateTotalsOf, type CandidateTotalsInput } from "../index";
|
|
13
|
+
|
|
14
|
+
function line(
|
|
15
|
+
sourceId: string,
|
|
16
|
+
overrides: Partial<CandidateTotalsInput> = {},
|
|
17
|
+
): CandidateTotalsInput {
|
|
18
|
+
return {
|
|
19
|
+
sourceId,
|
|
20
|
+
included: true,
|
|
21
|
+
treatmentUnresolved: false,
|
|
22
|
+
visibility: "shown",
|
|
23
|
+
machineState: "fresh",
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("candidateTotalsOf", () => {
|
|
29
|
+
it("counts distinct files, never lines", () => {
|
|
30
|
+
expect(
|
|
31
|
+
candidateTotalsOf([
|
|
32
|
+
line("a", { treatmentUnresolved: true }),
|
|
33
|
+
line("a"),
|
|
34
|
+
line("b", { included: false, treatmentUnresolved: true }),
|
|
35
|
+
line("c"),
|
|
36
|
+
]),
|
|
37
|
+
).toEqual({
|
|
38
|
+
files: 3,
|
|
39
|
+
filesIncluded: 2,
|
|
40
|
+
filesNeedingChoice: 1,
|
|
41
|
+
filesHidden: 0,
|
|
42
|
+
filesNotRediscovered: 0,
|
|
43
|
+
filesPendingClassification: 0,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("a file is hidden only when every line of it is hidden", () => {
|
|
48
|
+
const hidden = { visibility: "hidden", included: false } as const;
|
|
49
|
+
const totals = candidateTotalsOf([
|
|
50
|
+
line("partly", hidden),
|
|
51
|
+
line("partly"),
|
|
52
|
+
line("wholly", hidden),
|
|
53
|
+
line("wholly", hidden),
|
|
54
|
+
line("shown"),
|
|
55
|
+
]);
|
|
56
|
+
expect(totals.files).toBe(3);
|
|
57
|
+
expect(totals.filesHidden).toBe(1);
|
|
58
|
+
expect(totals.filesIncluded).toBe(2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("a file is not rediscovered or pending classification when any line of it is", () => {
|
|
62
|
+
const totals = candidateTotalsOf([
|
|
63
|
+
line("a", { machineState: "not_rediscovered" }),
|
|
64
|
+
line("a", { machineState: "refreshed" }),
|
|
65
|
+
line("b", { machineState: "pending_classification" }),
|
|
66
|
+
line("b"),
|
|
67
|
+
line("c", { machineState: "retired" }),
|
|
68
|
+
]);
|
|
69
|
+
expect(totals.filesNotRediscovered).toBe(1);
|
|
70
|
+
expect(totals.filesPendingClassification).toBe(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("a file routed to two documents counts once in every total", () => {
|
|
74
|
+
const facts = {
|
|
75
|
+
treatmentUnresolved: true,
|
|
76
|
+
visibility: "hidden",
|
|
77
|
+
machineState: "not_rediscovered",
|
|
78
|
+
included: false,
|
|
79
|
+
} as const;
|
|
80
|
+
const hiddenTwice = candidateTotalsOf([line("a", facts), line("a", facts)]);
|
|
81
|
+
expect(hiddenTwice).toEqual({
|
|
82
|
+
files: 1,
|
|
83
|
+
filesIncluded: 0,
|
|
84
|
+
filesNeedingChoice: 0,
|
|
85
|
+
filesHidden: 1,
|
|
86
|
+
filesNotRediscovered: 1,
|
|
87
|
+
filesPendingClassification: 0,
|
|
88
|
+
});
|
|
89
|
+
const shownTwice = candidateTotalsOf([
|
|
90
|
+
line("a", {
|
|
91
|
+
treatmentUnresolved: true,
|
|
92
|
+
machineState: "pending_classification",
|
|
93
|
+
}),
|
|
94
|
+
line("a", {
|
|
95
|
+
treatmentUnresolved: true,
|
|
96
|
+
machineState: "pending_classification",
|
|
97
|
+
}),
|
|
98
|
+
]);
|
|
99
|
+
expect(shownTwice).toEqual({
|
|
100
|
+
files: 1,
|
|
101
|
+
filesIncluded: 1,
|
|
102
|
+
filesNeedingChoice: 1,
|
|
103
|
+
filesHidden: 0,
|
|
104
|
+
filesNotRediscovered: 0,
|
|
105
|
+
filesPendingClassification: 1,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("counts nothing for an empty review", () => {
|
|
110
|
+
expect(candidateTotalsOf([])).toEqual({
|
|
111
|
+
files: 0,
|
|
112
|
+
filesIncluded: 0,
|
|
113
|
+
filesNeedingChoice: 0,
|
|
114
|
+
filesHidden: 0,
|
|
115
|
+
filesNotRediscovered: 0,
|
|
116
|
+
filesPendingClassification: 0,
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -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
|
-
|
|
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: {
|
|
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
|
|
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
|
}
|
package/src/source-sync/index.ts
CHANGED
|
@@ -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,
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source discovery RELEVANCE vocabulary (control ADR source-discovery-relevance-funnel).
|
|
3
|
+
* Closed sets that go on the wire AND into backend CHECK constraints. Renaming a member is a migration.
|
|
4
|
+
*
|
|
5
|
+
* Two axes: relevance (dropped | hidden | shown — the funnel's verdict) and the disclosure
|
|
6
|
+
* treatment (SOURCE_USES — a person's). Neither changes the other.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
/** The funnel's verdict. `dropped` is persisted (drop ledger / re-scored observation) and never reaches the wire item. */
|
|
12
|
+
export const SOURCE_RELEVANCE_TIERS = ["dropped", "hidden", "shown"] as const;
|
|
13
|
+
export type SourceRelevanceTier = (typeof SOURCE_RELEVANCE_TIERS)[number];
|
|
14
|
+
|
|
15
|
+
/** Whether a classifier has produced class/scope/period/sensitivity for the line. Separate from relevance. */
|
|
16
|
+
export const CLASSIFICATION_STATES = ["unclassified", "classified"] as const;
|
|
17
|
+
export type ClassificationState = (typeof CLASSIFICATION_STATES)[number];
|
|
18
|
+
|
|
19
|
+
/** RelevanceReason is a discriminated union on `kind`; each kind has its own disjoint code set. */
|
|
20
|
+
export const RELEVANCE_REASON_KINDS = [
|
|
21
|
+
"lexicon",
|
|
22
|
+
"signal",
|
|
23
|
+
"evaluation",
|
|
24
|
+
"model",
|
|
25
|
+
] as const;
|
|
26
|
+
export type RelevanceReasonKind = (typeof RELEVANCE_REASON_KINDS)[number];
|
|
27
|
+
|
|
28
|
+
/** A product-owned lexicon TERM matched (the term travels; a filename slice never does). */
|
|
29
|
+
export const LEXICON_REASON_CODES = [
|
|
30
|
+
"title_term",
|
|
31
|
+
"folder_term",
|
|
32
|
+
"negative_title_term",
|
|
33
|
+
"negative_folder_term",
|
|
34
|
+
] as const;
|
|
35
|
+
export type LexiconReasonCode = (typeof LEXICON_REASON_CODES)[number];
|
|
36
|
+
|
|
37
|
+
/** A metadata signal with a numeric value (or null). `version_churn` is Drive's server-side version, never an edit count. */
|
|
38
|
+
export const SIGNAL_REASON_CODES = [
|
|
39
|
+
"folder_probe_hit",
|
|
40
|
+
"title_probe_hit",
|
|
41
|
+
"version_churn",
|
|
42
|
+
"mime_class",
|
|
43
|
+
"size_band",
|
|
44
|
+
"dated_series",
|
|
45
|
+
"recent_edit",
|
|
46
|
+
"owner_unit_head",
|
|
47
|
+
"owner_executive",
|
|
48
|
+
"sharing_breadth",
|
|
49
|
+
"duplicate_title",
|
|
50
|
+
"personal_mime",
|
|
51
|
+
] as const;
|
|
52
|
+
export type SignalReasonCode = (typeof SIGNAL_REASON_CODES)[number];
|
|
53
|
+
|
|
54
|
+
/** What was NOT evaluated, or was explored. Never negative evidence (INV-SOURCESYNC-MISSING-EVIDENCE-IS-NOT-EVIDENCE). */
|
|
55
|
+
export const EVALUATION_REASON_CODES = [
|
|
56
|
+
"not_evaluated_t1",
|
|
57
|
+
"not_evaluated_t2",
|
|
58
|
+
"folder_unresolved",
|
|
59
|
+
"folder_truncated",
|
|
60
|
+
"explored_t1",
|
|
61
|
+
"explored_t2",
|
|
62
|
+
] as const;
|
|
63
|
+
export type EvaluationReasonCode = (typeof EVALUATION_REASON_CODES)[number];
|
|
64
|
+
|
|
65
|
+
/** A model tier's contribution. */
|
|
66
|
+
export const MODEL_REASON_CODES = [
|
|
67
|
+
"title_path_score",
|
|
68
|
+
"peek_confirms",
|
|
69
|
+
"peek_contradicts",
|
|
70
|
+
] as const;
|
|
71
|
+
export type ModelReasonCode = (typeof MODEL_REASON_CODES)[number];
|
|
72
|
+
|
|
73
|
+
/** The audience Attach would disclose the source to. Mirrors the backend's TARGET_BREADTHS. `restricted` ≠ `restricted_few`. */
|
|
74
|
+
export const TARGET_AUDIENCES = ["restricted", "unit", "org_wide"] as const;
|
|
75
|
+
export type TargetAudience = (typeof TARGET_AUDIENCES)[number];
|
|
76
|
+
|
|
77
|
+
/** Why a tier got the model's attention (INV-SOURCESYNC-EXPLORATION-RESERVED). */
|
|
78
|
+
export const ATTENTION_LANES = ["exploit", "explore"] as const;
|
|
79
|
+
export type AttentionLane = (typeof ATTENTION_LANES)[number];
|
|
80
|
+
|
|
81
|
+
/** A candidate line's machine lifecycle (INV-SOURCESYNC-SET-IS-RECONCILED, INV-SOURCESYNC-DROP-IS-TWO-LIFECYCLES). `retired` = positive absence only. */
|
|
82
|
+
export const CANDIDATE_MACHINE_STATES = [
|
|
83
|
+
"fresh",
|
|
84
|
+
"refreshed",
|
|
85
|
+
"filtered_out",
|
|
86
|
+
"not_rediscovered",
|
|
87
|
+
"pending_classification",
|
|
88
|
+
"retired",
|
|
89
|
+
] as const;
|
|
90
|
+
export type CandidateMachineState = (typeof CANDIDATE_MACHINE_STATES)[number];
|
|
91
|
+
|
|
92
|
+
/** Provenance of a committed treatment (INV-SOURCESYNC-RECOMMENDATION-IS-NOT-COMMITMENT). */
|
|
93
|
+
export const SELECTION_SOURCES = ["person", "approval_default"] as const;
|
|
94
|
+
export type SelectionSource = (typeof SELECTION_SOURCES)[number];
|
|
95
|
+
|
|
96
|
+
/** Field-level write barriers a person's action places on a line (INV-SOURCESYNC-HOLDS-ARE-FIELD-BARRIERS). */
|
|
97
|
+
export const PERSON_HOLD_REASONS = [
|
|
98
|
+
"relevance_override",
|
|
99
|
+
"treatment_selection",
|
|
100
|
+
"target_override",
|
|
101
|
+
"manual_add",
|
|
102
|
+
] as const;
|
|
103
|
+
export type PersonHoldReason = (typeof PERSON_HOLD_REASONS)[number];
|
|
104
|
+
|
|
105
|
+
/** How a discovery scan ended. */
|
|
106
|
+
export const SCAN_OUTCOMES = [
|
|
107
|
+
"complete",
|
|
108
|
+
"budget_exhausted",
|
|
109
|
+
"file_ceiling",
|
|
110
|
+
"stopped",
|
|
111
|
+
] as const;
|
|
112
|
+
export type ScanOutcome = (typeof SCAN_OUTCOMES)[number];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Why a candidate scored as it did. A discriminated union: the drop ledger keeps only {kind, code}
|
|
116
|
+
* and counts; product copy switches exhaustively on `kind` then `code`. A lexicon `term` is ALWAYS a
|
|
117
|
+
* product-owned lexicon entry, never a slice of the file name (INV-SOURCESYNC-POLICY-IS-RECORDED).
|
|
118
|
+
*/
|
|
119
|
+
export const RelevanceReasonSchema = z.discriminatedUnion("kind", [
|
|
120
|
+
z.object({
|
|
121
|
+
kind: z.literal("lexicon"),
|
|
122
|
+
code: z.enum(LEXICON_REASON_CODES),
|
|
123
|
+
term: z.string().min(1).max(64),
|
|
124
|
+
count: z.number().int().min(1),
|
|
125
|
+
}),
|
|
126
|
+
z.object({
|
|
127
|
+
kind: z.literal("signal"),
|
|
128
|
+
code: z.enum(SIGNAL_REASON_CODES),
|
|
129
|
+
value: z.number().nullable(),
|
|
130
|
+
}),
|
|
131
|
+
z.object({
|
|
132
|
+
kind: z.literal("evaluation"),
|
|
133
|
+
code: z.enum(EVALUATION_REASON_CODES),
|
|
134
|
+
}),
|
|
135
|
+
z.object({
|
|
136
|
+
kind: z.literal("model"),
|
|
137
|
+
code: z.enum(MODEL_REASON_CODES),
|
|
138
|
+
score: z.number().min(0).max(1).nullable(),
|
|
139
|
+
}),
|
|
140
|
+
]);
|
|
141
|
+
export type RelevanceReason = z.infer<typeof RelevanceReasonSchema>;
|
|
142
|
+
|
|
143
|
+
/** Which lane earned each model tier's attention for a line; null = that tier did not look at it. */
|
|
144
|
+
export const CandidateAttentionSchema = z.object({
|
|
145
|
+
t1: z.enum(ATTENTION_LANES).nullable(),
|
|
146
|
+
t2: z.enum(ATTENTION_LANES).nullable(),
|
|
147
|
+
});
|
|
148
|
+
export type CandidateAttention = z.infer<typeof CandidateAttentionSchema>;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The last completed discovery run's counts, so no surface shows one ambiguous total.
|
|
152
|
+
* The set response carries it NULL before any run has completed.
|
|
153
|
+
*/
|
|
154
|
+
export const CandidateDiscoverySummarySchema = z.object({
|
|
155
|
+
foundThisRun: z.number().int().min(0),
|
|
156
|
+
retainedDecisions: z.number().int().min(0),
|
|
157
|
+
notRediscovered: z.number().int().min(0),
|
|
158
|
+
pendingClassification: z.number().int().min(0),
|
|
159
|
+
filesDropped: z.number().int().min(0),
|
|
160
|
+
coveragePercent: z.number().int().min(0).max(100),
|
|
161
|
+
scanOutcome: z.enum(SCAN_OUTCOMES),
|
|
162
|
+
});
|
|
163
|
+
export type CandidateDiscoverySummary = z.infer<
|
|
164
|
+
typeof CandidateDiscoverySummarySchema
|
|
165
|
+
>;
|
|
166
|
+
|
|
167
|
+
/** The item fields the relevance consistency rules read. */
|
|
168
|
+
type CandidateRelevanceFacts = {
|
|
169
|
+
visibility: SourceRelevanceTier;
|
|
170
|
+
included: boolean;
|
|
171
|
+
classificationState: ClassificationState;
|
|
172
|
+
recommendedUse: string | null;
|
|
173
|
+
sourceClass: string | null;
|
|
174
|
+
use: string | null;
|
|
175
|
+
selectionSource: SelectionSource | null;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The candidate item's cross-field rules (control ADR source-discovery-relevance-funnel):
|
|
180
|
+
* 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.
|
|
182
|
+
*/
|
|
183
|
+
export function refineCandidateRelevance(
|
|
184
|
+
item: CandidateRelevanceFacts,
|
|
185
|
+
ctx: z.RefinementCtx,
|
|
186
|
+
): void {
|
|
187
|
+
const unclassified = item.classificationState === "unclassified";
|
|
188
|
+
const hasRecommendation =
|
|
189
|
+
item.recommendedUse !== null || item.sourceClass !== null;
|
|
190
|
+
const missingRecommendation =
|
|
191
|
+
item.recommendedUse === null || item.sourceClass === null;
|
|
192
|
+
if (item.visibility === "hidden" && item.included) {
|
|
193
|
+
ctx.addIssue({
|
|
194
|
+
code: "custom",
|
|
195
|
+
message: "a hidden line is never included",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (unclassified && hasRecommendation) {
|
|
199
|
+
ctx.addIssue({
|
|
200
|
+
code: "custom",
|
|
201
|
+
message: "an unclassified line has no recommendation and no class",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (!unclassified && missingRecommendation) {
|
|
205
|
+
ctx.addIssue({
|
|
206
|
+
code: "custom",
|
|
207
|
+
message: "a classified line has a recommendation and a class",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if ((item.use === null) !== (item.selectionSource === null)) {
|
|
211
|
+
ctx.addIssue({
|
|
212
|
+
code: "custom",
|
|
213
|
+
message: "selectionSource is present exactly when use is",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -30,6 +30,18 @@ import {
|
|
|
30
30
|
SOURCE_SYNC_SCOPE_APPROVER_KINDS,
|
|
31
31
|
SOURCE_USES,
|
|
32
32
|
} 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";
|
|
33
45
|
|
|
34
46
|
/** Integer micro-USD as a string — the billing domain's money convention on the wire. */
|
|
35
47
|
const MicroUsd = z.string().regex(/^[0-9]+$/);
|
|
@@ -77,62 +89,86 @@ export const SourceSyncDiscoveryRunResponseSchema = z.object({
|
|
|
77
89
|
});
|
|
78
90
|
|
|
79
91
|
/** ONE source proposed for ONE target document. */
|
|
80
|
-
export const SourceSyncCandidateItemSchema = z
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
.
|
|
135
|
-
|
|
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);
|
|
136
172
|
|
|
137
173
|
/** Distinct-file totals over every line of a candidate set the caller may see. */
|
|
138
174
|
export const SourceSyncCandidateTotalsSchema = z.object({
|
|
@@ -142,6 +178,12 @@ export const SourceSyncCandidateTotalsSchema = z.object({
|
|
|
142
178
|
filesIncluded: z.number().int().min(0),
|
|
143
179
|
/** Files with at least one included line whose treatment is unresolved. */
|
|
144
180
|
filesNeedingChoice: z.number().int().min(0),
|
|
181
|
+
/** Files every line of which is hidden (behind the drawer; control ADR source-discovery-relevance-funnel). */
|
|
182
|
+
filesHidden: z.number().int().min(0),
|
|
183
|
+
/** Files with at least one line the latest run did not see again. */
|
|
184
|
+
filesNotRediscovered: z.number().int().min(0),
|
|
185
|
+
/** Files with at least one line awaiting classification. */
|
|
186
|
+
filesPendingClassification: z.number().int().min(0),
|
|
145
187
|
});
|
|
146
188
|
|
|
147
189
|
/** The draft set under review. `revision` moves on every edit and is echoed on approve. */
|
|
@@ -170,14 +212,19 @@ export const SourceSyncCandidateSetResponseSchema = z.object({
|
|
|
170
212
|
capMicroUsd: MicroUsd,
|
|
171
213
|
})
|
|
172
214
|
.nullable(),
|
|
215
|
+
/** The last completed discovery run's counts; null before any run completed. */
|
|
216
|
+
discovery: z.nullable(CandidateDiscoverySummarySchema),
|
|
173
217
|
});
|
|
174
218
|
|
|
175
219
|
/**
|
|
176
|
-
* One edit to one candidate item
|
|
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.
|
|
177
222
|
* The SERVER bumps the set's revision on every accepted edit; the client never sends one here.
|
|
178
223
|
*/
|
|
179
224
|
export const SourceSyncCandidateItemPatchSchema = z
|
|
180
225
|
.object({
|
|
226
|
+
/** Bring into review: hidden → shown. Changes VISIBILITY ONLY (INV-SOURCESYNC-RECOMMENDATION-IS-NOT-COMMITMENT). */
|
|
227
|
+
visibility: z.literal("shown").optional(),
|
|
181
228
|
/** Attach, direction only, or leave out. Choosing one on a `widens` item RESOLVES its treatment. */
|
|
182
229
|
use: z.enum(SOURCE_USES).optional(),
|
|
183
230
|
/** Correct the class the classifier chose. */
|
|
@@ -186,8 +233,8 @@ export const SourceSyncCandidateItemPatchSchema = z
|
|
|
186
233
|
targetDocId: z.string().uuid().optional(),
|
|
187
234
|
})
|
|
188
235
|
.strict()
|
|
189
|
-
.refine((patch) => Object.keys(patch).length
|
|
190
|
-
message: "a patch changes
|
|
236
|
+
.refine((patch) => Object.keys(patch).length === 1, {
|
|
237
|
+
message: "a patch changes exactly one thing",
|
|
191
238
|
});
|
|
192
239
|
|
|
193
240
|
/** "Add one I missed": a file the person picked in the provider's own search. */
|