@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.
- package/package.json +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +54 -3
- package/src/decisions/README.md +23 -0
- package/src/decisions/__tests__/README.md +10 -1
- package/src/decisions/__tests__/decision-schemas-golden.test.ts +6 -0
- package/src/decisions/__tests__/fixtures/decision-schemas.golden.json +2 -6
- package/src/decisions/__tests__/question-evidence-optional.test.ts +101 -0
- package/src/decisions/schemas.ts +2 -2
- package/src/decisions/types.ts +13 -5
- 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
|
@@ -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. */
|