@company-semantics/contracts 88.2.0 → 89.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 +337 -2
- package/src/generated/openapi-routes.ts +4 -0
- package/src/source-sync/README.md +44 -2
- package/src/source-sync/__tests__/barrel.test.ts +11 -1
- package/src/source-sync/__tests__/schemas.test.ts +141 -2
- package/src/source-sync/__tests__/scope-projection.test.ts +1 -1
- package/src/source-sync/__tests__/vocabulary.test.ts +73 -1
- package/src/source-sync/candidate-item.ts +155 -0
- package/src/source-sync/candidate-totals.ts +2 -4
- package/src/source-sync/index.ts +13 -4
- package/src/source-sync/relevance.ts +38 -3
- package/src/source-sync/schemas.ts +5 -127
- package/src/source-sync/vocabulary.ts +45 -0
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
* - Relevance and treatment are two axes (control ADR source-discovery-relevance-funnel):
|
|
15
15
|
* a hidden line is never included; an unclassified line has no recommendation and
|
|
16
16
|
* no class; `selectionSource` is present exactly when `use` is.
|
|
17
|
+
* - The resolved document type (control ADR source-discovery-observation-vs-policy)
|
|
18
|
+
* is a REQUIRED nullable key: an unclassified line has none, and a confidence
|
|
19
|
+
* travels exactly with a known type. A line carries at most sixteen reasons.
|
|
17
20
|
* - A relevance reason's code belongs to its kind's closed set.
|
|
18
21
|
* - A candidate patch changes exactly one thing: bringing a line into review never
|
|
19
22
|
* also commits a treatment, and a patch can never hide a line.
|
|
@@ -28,6 +31,8 @@ import {
|
|
|
28
31
|
SourceSyncCandidateSetResponseSchema,
|
|
29
32
|
SourceSyncDiscoveryRunResponseSchema,
|
|
30
33
|
} from "../index";
|
|
34
|
+
import * as root from "../../index";
|
|
35
|
+
import * as candidateItemModule from "../candidate-item";
|
|
31
36
|
|
|
32
37
|
const RUN_ID = "3f1a2b4c-5d6e-4f70-8192-a3b4c5d6e7f8";
|
|
33
38
|
const WORKFLOW_ID = "4a2b3c5d-6e7f-4a81-9b2c-b4c5d6e7f8a9";
|
|
@@ -52,6 +57,8 @@ function candidateItem(overrides: Record<string, unknown> = {}) {
|
|
|
52
57
|
visibility: "shown",
|
|
53
58
|
classificationState: "classified",
|
|
54
59
|
sourceClass: "board_deck",
|
|
60
|
+
documentType: null,
|
|
61
|
+
documentTypeConfidence: null,
|
|
55
62
|
periodLabel: "Q3 2026",
|
|
56
63
|
seriesRole: "current",
|
|
57
64
|
relevance: 0.92,
|
|
@@ -142,6 +149,18 @@ function candidateSet(overrides: Record<string, unknown> = {}) {
|
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
describe("SourceSyncCandidateItemSchema", () => {
|
|
152
|
+
it("the candidate item schema moved without changing what it accepts", () => {
|
|
153
|
+
const parsed = SourceSyncCandidateItemSchema.safeParse(candidateItem());
|
|
154
|
+
expect(parsed.success).toBe(true);
|
|
155
|
+
expect(parsed.data).toEqual(candidateItem());
|
|
156
|
+
expect(root.SourceSyncCandidateItemSchema).toBe(
|
|
157
|
+
candidateItemModule.SourceSyncCandidateItemSchema,
|
|
158
|
+
);
|
|
159
|
+
expect(root.SourceSyncCandidateItemPatchSchema).toBe(
|
|
160
|
+
candidateItemModule.SourceSyncCandidateItemPatchSchema,
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
145
164
|
it("refuses a candidate item with no audience fit key at all", () => {
|
|
146
165
|
const { audienceFit: _omitted, ...item } = candidateItem();
|
|
147
166
|
const parsed = SourceSyncCandidateItemSchema.safeParse(item);
|
|
@@ -362,15 +381,104 @@ describe("candidate relevance", () => {
|
|
|
362
381
|
]);
|
|
363
382
|
});
|
|
364
383
|
|
|
365
|
-
it("
|
|
384
|
+
it("accepts sixteen reasons", () => {
|
|
366
385
|
const reason = { kind: "evaluation", code: "explored_t1" };
|
|
367
386
|
expect(
|
|
368
387
|
SourceSyncCandidateItemSchema.safeParse(
|
|
369
|
-
candidateItem({ relevanceReasons: Array(
|
|
388
|
+
candidateItem({ relevanceReasons: Array(16).fill(reason) }),
|
|
389
|
+
).success,
|
|
390
|
+
).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("refuses more than sixteen reasons", () => {
|
|
394
|
+
const reason = { kind: "evaluation", code: "explored_t1" };
|
|
395
|
+
expect(
|
|
396
|
+
SourceSyncCandidateItemSchema.safeParse(
|
|
397
|
+
candidateItem({ relevanceReasons: Array(17).fill(reason) }),
|
|
398
|
+
).success,
|
|
399
|
+
).toBe(false);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("a typed item carries its resolved document type and confidence", () => {
|
|
403
|
+
const parsed = SourceSyncCandidateItemSchema.safeParse(
|
|
404
|
+
candidateItem({
|
|
405
|
+
documentType: "corporate_formation_record",
|
|
406
|
+
documentTypeConfidence: 0.92,
|
|
407
|
+
sourceClass: "other",
|
|
408
|
+
}),
|
|
409
|
+
);
|
|
410
|
+
expect(parsed.success).toBe(true);
|
|
411
|
+
expect(parsed.data?.documentType).toBe("corporate_formation_record");
|
|
412
|
+
expect(parsed.data?.documentTypeConfidence).toBe(0.92);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("a v1-pinned classified item has no document type", () => {
|
|
416
|
+
const parsed = SourceSyncCandidateItemSchema.safeParse(
|
|
417
|
+
candidateItem({ sourceClass: "board_minutes" }),
|
|
418
|
+
);
|
|
419
|
+
expect(parsed.success).toBe(true);
|
|
420
|
+
expect(parsed.data?.documentType).toBeNull();
|
|
421
|
+
expect(parsed.data?.documentTypeConfidence).toBeNull();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("an unclassified line has no document type", () => {
|
|
425
|
+
const parsed = SourceSyncCandidateItemSchema.safeParse(
|
|
426
|
+
candidateItem({
|
|
427
|
+
visibility: "hidden",
|
|
428
|
+
classificationState: "unclassified",
|
|
429
|
+
recommendedUse: null,
|
|
430
|
+
sourceClass: null,
|
|
431
|
+
included: false,
|
|
432
|
+
documentType: "strategy_memo",
|
|
433
|
+
documentTypeConfidence: 0.9,
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
expect(parsed.success).toBe(false);
|
|
437
|
+
expect(parsed.error?.issues[0]?.message).toBe(
|
|
438
|
+
"an unclassified line has no document type",
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("a known document type carries a confidence; unknown and absent carry none", () => {
|
|
443
|
+
for (const [documentType, documentTypeConfidence, ok] of [
|
|
444
|
+
["strategy_memo", 0.9, true],
|
|
445
|
+
["strategy_memo", null, false],
|
|
446
|
+
["unknown", null, true],
|
|
447
|
+
["unknown", 0.4, false],
|
|
448
|
+
[null, null, true],
|
|
449
|
+
[null, 0.4, false],
|
|
450
|
+
] as const) {
|
|
451
|
+
const parsed = SourceSyncCandidateItemSchema.safeParse(
|
|
452
|
+
candidateItem({ documentType, documentTypeConfidence }),
|
|
453
|
+
);
|
|
454
|
+
expect(parsed.success).toBe(ok);
|
|
455
|
+
if (!ok) {
|
|
456
|
+
expect(parsed.error?.issues[0]?.message).toBe(
|
|
457
|
+
"a known document type carries a confidence; unknown and absent carry none",
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("refuses a document type outside the closed set", () => {
|
|
464
|
+
expect(
|
|
465
|
+
SourceSyncCandidateItemSchema.safeParse(
|
|
466
|
+
candidateItem({
|
|
467
|
+
documentType: "irrelevant_doc",
|
|
468
|
+
documentTypeConfidence: 0.9,
|
|
469
|
+
}),
|
|
370
470
|
).success,
|
|
371
471
|
).toBe(false);
|
|
372
472
|
});
|
|
373
473
|
|
|
474
|
+
it("the document type keys are required", () => {
|
|
475
|
+
for (const key of ["documentType", "documentTypeConfidence"]) {
|
|
476
|
+
const item: Record<string, unknown> = candidateItem();
|
|
477
|
+
delete item[key];
|
|
478
|
+
expect(SourceSyncCandidateItemSchema.safeParse(item).success).toBe(false);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
374
482
|
it("a reason whose code belongs to another kind is refused", () => {
|
|
375
483
|
for (const reason of [
|
|
376
484
|
{ kind: "lexicon", code: "version_churn", term: "board", count: 1 },
|
|
@@ -382,6 +490,37 @@ describe("candidate relevance", () => {
|
|
|
382
490
|
}
|
|
383
491
|
});
|
|
384
492
|
|
|
493
|
+
it("accepts the typed-relevance reason codes", () => {
|
|
494
|
+
const relevanceReasons = [
|
|
495
|
+
{ kind: "model", code: "type_policy", score: 0.05 },
|
|
496
|
+
{ kind: "model", code: "peek_strategy_evidence_present", score: 0.8 },
|
|
497
|
+
{ kind: "model", code: "peek_strategy_evidence_absent", score: 0.1 },
|
|
498
|
+
{ kind: "evaluation", code: "peek_insufficient_evidence" },
|
|
499
|
+
{ kind: "evaluation", code: "type_disagreement" },
|
|
500
|
+
{ kind: "evaluation", code: "type_below_confidence" },
|
|
501
|
+
{ kind: "evaluation", code: "execution_failed_t1" },
|
|
502
|
+
{ kind: "evaluation", code: "execution_failed_t2" },
|
|
503
|
+
];
|
|
504
|
+
expect(
|
|
505
|
+
SourceSyncCandidateItemSchema.safeParse(
|
|
506
|
+
candidateItem({ relevanceReasons }),
|
|
507
|
+
).success,
|
|
508
|
+
).toBe(true);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("a typed-relevance code under the wrong kind is refused", () => {
|
|
512
|
+
for (const reason of [
|
|
513
|
+
{ kind: "evaluation", code: "type_policy" },
|
|
514
|
+
{ kind: "model", code: "type_disagreement", score: 0.5 },
|
|
515
|
+
]) {
|
|
516
|
+
expect(
|
|
517
|
+
SourceSyncCandidateItemSchema.safeParse(
|
|
518
|
+
candidateItem({ relevanceReasons: [reason] }),
|
|
519
|
+
).success,
|
|
520
|
+
).toBe(false);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
385
524
|
it("refuses a lexicon reason without a term or a count, and a model score outside 0..1", () => {
|
|
386
525
|
for (const reason of [
|
|
387
526
|
{ kind: "lexicon", code: "title_term", count: 1 },
|
|
@@ -16,12 +16,12 @@ import { DecisionEvidenceRowSchema } from "../../decisions/presentation";
|
|
|
16
16
|
import {
|
|
17
17
|
SourceSyncApplyResponseSchema,
|
|
18
18
|
SourceSyncApproveResponseSchema,
|
|
19
|
-
SourceSyncCandidateItemSchema,
|
|
20
19
|
SourceSyncCandidateSetResponseSchema,
|
|
21
20
|
SourceSyncDocChangeSchema,
|
|
22
21
|
SourceSyncScopeApprovalProjectionSchema,
|
|
23
22
|
SourceSyncSynthesisResponseSchema,
|
|
24
23
|
} from "../schemas";
|
|
24
|
+
import { SourceSyncCandidateItemSchema } from "../candidate-item";
|
|
25
25
|
|
|
26
26
|
const UNIT_SCOPE = "unit:3f1c2d4e-5a6b-4c7d-8e9f-a0b1c2d3e4f5";
|
|
27
27
|
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* - `decoy` is a source class; `unknown` is a sharing breadth
|
|
12
12
|
* - No member string appears in two sets
|
|
13
13
|
* - The four relevance reason code sets are pairwise disjoint
|
|
14
|
+
* - DOCUMENT_TYPES has exactly its thirty members, `unknown` last; it sits
|
|
15
|
+
* OUTSIDE the disjointness table and shares a word with another set only
|
|
16
|
+
* where pinned (a structural name on a separate axis)
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
import { describe, it, expect } from "vitest";
|
|
@@ -33,6 +36,7 @@ import {
|
|
|
33
36
|
|
|
34
37
|
import {
|
|
35
38
|
AUDIENCE_FITS,
|
|
39
|
+
DOCUMENT_TYPES,
|
|
36
40
|
SHARING_BREADTHS,
|
|
37
41
|
SOURCE_CLASSES,
|
|
38
42
|
SOURCE_SENSITIVITY_CLASSES,
|
|
@@ -175,12 +179,24 @@ const VOCABULARIES = [
|
|
|
175
179
|
"folder_truncated",
|
|
176
180
|
"explored_t1",
|
|
177
181
|
"explored_t2",
|
|
182
|
+
"peek_insufficient_evidence",
|
|
183
|
+
"type_disagreement",
|
|
184
|
+
"type_below_confidence",
|
|
185
|
+
"execution_failed_t1",
|
|
186
|
+
"execution_failed_t2",
|
|
178
187
|
],
|
|
179
188
|
},
|
|
180
189
|
{
|
|
181
190
|
name: "MODEL_REASON_CODES",
|
|
182
191
|
tuple: MODEL_REASON_CODES,
|
|
183
|
-
members: [
|
|
192
|
+
members: [
|
|
193
|
+
"title_path_score",
|
|
194
|
+
"peek_confirms",
|
|
195
|
+
"peek_contradicts",
|
|
196
|
+
"type_policy",
|
|
197
|
+
"peek_strategy_evidence_present",
|
|
198
|
+
"peek_strategy_evidence_absent",
|
|
199
|
+
],
|
|
184
200
|
},
|
|
185
201
|
{
|
|
186
202
|
name: "TARGET_AUDIENCES",
|
|
@@ -277,3 +293,59 @@ describe("Source Sync closed sets", () => {
|
|
|
277
293
|
});
|
|
278
294
|
});
|
|
279
295
|
});
|
|
296
|
+
|
|
297
|
+
describe("document types", () => {
|
|
298
|
+
it("has exactly the thirty document types, in order", () => {
|
|
299
|
+
expect([...DOCUMENT_TYPES]).toEqual([
|
|
300
|
+
"board_deck",
|
|
301
|
+
"board_minutes",
|
|
302
|
+
"strategy_memo",
|
|
303
|
+
"planning_doc",
|
|
304
|
+
"okr_doc",
|
|
305
|
+
"roadmap",
|
|
306
|
+
"operating_review",
|
|
307
|
+
"all_hands_deck",
|
|
308
|
+
"offsite_notes",
|
|
309
|
+
"investor_update",
|
|
310
|
+
"pitch_deck",
|
|
311
|
+
"product_spec",
|
|
312
|
+
"research_analysis",
|
|
313
|
+
"general_meeting_notes",
|
|
314
|
+
"financial_plan",
|
|
315
|
+
"financial_actuals",
|
|
316
|
+
"equity_admin_record",
|
|
317
|
+
"corporate_formation_record",
|
|
318
|
+
"corporate_governance_record",
|
|
319
|
+
"government_filing",
|
|
320
|
+
"equity_agreement",
|
|
321
|
+
"commercial_agreement",
|
|
322
|
+
"employment_agreement",
|
|
323
|
+
"transaction_record",
|
|
324
|
+
"personal_document",
|
|
325
|
+
"social_event_material",
|
|
326
|
+
"coursework",
|
|
327
|
+
"empty_template",
|
|
328
|
+
"image_or_screenshot",
|
|
329
|
+
"unknown",
|
|
330
|
+
]);
|
|
331
|
+
expect(new Set(DOCUMENT_TYPES).size).toBe(30);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("unknown is the last document type", () => {
|
|
335
|
+
expect(DOCUMENT_TYPES[DOCUMENT_TYPES.length - 1]).toBe("unknown");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("document types share a member with another set only where pinned", () => {
|
|
339
|
+
const others = new Set<string>(
|
|
340
|
+
VOCABULARIES.flatMap(({ tuple }) => [...tuple]),
|
|
341
|
+
);
|
|
342
|
+
const shared = DOCUMENT_TYPES.filter((t) => others.has(t)).sort();
|
|
343
|
+
expect(shared).toEqual([
|
|
344
|
+
"board_deck",
|
|
345
|
+
"board_minutes",
|
|
346
|
+
"investor_update",
|
|
347
|
+
"roadmap",
|
|
348
|
+
"unknown",
|
|
349
|
+
]);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Source Sync candidate item and its one-edit patch.
|
|
3
|
+
*
|
|
4
|
+
* A candidate item is ONE source proposed for ONE target document, because
|
|
5
|
+
* treatment and landing are source x target (INV-SOURCESYNC-LANDING-PER-TARGET).
|
|
6
|
+
* It carries facts ABOUT a source and a COUNT of people it is shared with, never
|
|
7
|
+
* its content and never their addresses. Absent values are `.nullable()`, never
|
|
8
|
+
* `.optional()`, so a response has one stable shape.
|
|
9
|
+
*
|
|
10
|
+
* The patch is a request body and is `.strict()`: an unknown key is an ERROR.
|
|
11
|
+
*
|
|
12
|
+
* Split out of `schemas.ts` (which keeps the set response that lists items) so
|
|
13
|
+
* that file stays within its size envelope.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { IsoDateTime } from "../api/primitives";
|
|
18
|
+
import {
|
|
19
|
+
AUDIENCE_FITS,
|
|
20
|
+
DOCUMENT_TYPES,
|
|
21
|
+
SHARING_BREADTHS,
|
|
22
|
+
SOURCE_CLASSES,
|
|
23
|
+
SOURCE_SENSITIVITY_CLASSES,
|
|
24
|
+
SOURCE_USES,
|
|
25
|
+
} from "./vocabulary";
|
|
26
|
+
import {
|
|
27
|
+
CANDIDATE_MACHINE_STATES,
|
|
28
|
+
CandidateAttentionSchema,
|
|
29
|
+
CLASSIFICATION_STATES,
|
|
30
|
+
PERSON_HOLD_REASONS,
|
|
31
|
+
refineCandidateRelevance,
|
|
32
|
+
RelevanceReasonSchema,
|
|
33
|
+
SELECTION_SOURCES,
|
|
34
|
+
SOURCE_RELEVANCE_TIERS,
|
|
35
|
+
TARGET_AUDIENCES,
|
|
36
|
+
} from "./relevance";
|
|
37
|
+
|
|
38
|
+
/** ONE source proposed for ONE target document. */
|
|
39
|
+
export const SourceSyncCandidateItemSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
itemId: z.string().uuid(),
|
|
42
|
+
sourceId: z.string().uuid(),
|
|
43
|
+
/**
|
|
44
|
+
* Display facts about the source. Never its content. When `withheld`, `name` is
|
|
45
|
+
* product copy naming only the source's class and period.
|
|
46
|
+
*/
|
|
47
|
+
name: z.string().min(1).max(500),
|
|
48
|
+
mimeType: z.string().min(1).max(255),
|
|
49
|
+
/** Opens the source IN THE PROVIDER, which remains the ACL authority for it. */
|
|
50
|
+
webViewLink: z.string().url().nullable(),
|
|
51
|
+
ownerLabel: z.string().max(255).nullable(),
|
|
52
|
+
modifiedTime: IsoDateTime.nullable(),
|
|
53
|
+
/**
|
|
54
|
+
* True when the caller may not open this source: its identifying fields are
|
|
55
|
+
* withheld (`webViewLink`, `ownerLabel` and `sharedWithCount` are null, and
|
|
56
|
+
* `name` and `matchReason` are product copy). What the approver decides stays.
|
|
57
|
+
*/
|
|
58
|
+
withheld: z.boolean(),
|
|
59
|
+
/** 'company' or 'unit:<uuid>'. */
|
|
60
|
+
scopeKey: z.string().min(1).max(64),
|
|
61
|
+
targetDocId: z.string().uuid(),
|
|
62
|
+
targetDocTitle: z.string().min(1).max(255),
|
|
63
|
+
/** The funnel's verdict for the wire: `dropped` never reaches it. Hidden = behind the drawer, undecided. */
|
|
64
|
+
visibility: z.enum(SOURCE_RELEVANCE_TIERS).exclude(["dropped"]),
|
|
65
|
+
/** Separate from visibility: a shown line may still await classification (machineState pending_classification). */
|
|
66
|
+
classificationState: z.enum(CLASSIFICATION_STATES),
|
|
67
|
+
/** NULL until a classifier has run for this line. */
|
|
68
|
+
sourceClass: z.enum(SOURCE_CLASSES).nullable(),
|
|
69
|
+
/**
|
|
70
|
+
* The RESOLVED document type: what the source IS, after the backend reconciles the model's T1 and
|
|
71
|
+
* T2 observations deterministically (control ADR source-discovery-observation-vs-policy). A NAME,
|
|
72
|
+
* never a verdict — the relevance policy it maps to is backend product policy and never on the wire.
|
|
73
|
+
* NULL on an unclassified line and on every line of a v1-pinned run.
|
|
74
|
+
*/
|
|
75
|
+
documentType: z.enum(DOCUMENT_TYPES).nullable(),
|
|
76
|
+
/** Confidence of the resolved type, 0..1. NULL exactly when documentType is null or `unknown`. */
|
|
77
|
+
documentTypeConfidence: z.number().min(0).max(1).nullable(),
|
|
78
|
+
periodLabel: z.string().max(64).nullable(),
|
|
79
|
+
seriesRole: z.enum(["current", "history"]).nullable(),
|
|
80
|
+
/** The FUSED relevance score (control ADR source-discovery-relevance-funnel), 0..1. */
|
|
81
|
+
relevance: z.number().min(0).max(1),
|
|
82
|
+
sensitivityClass: z.enum(SOURCE_SENSITIVITY_CLASSES),
|
|
83
|
+
sharingBreadth: z.enum(SHARING_BREADTHS),
|
|
84
|
+
/** A COUNT of people the provider shares it with, when observed. Never addresses. */
|
|
85
|
+
sharedWithCount: z.number().int().min(0).nullable(),
|
|
86
|
+
/**
|
|
87
|
+
* Whether attaching under THIS target would show the source to more people than the provider does.
|
|
88
|
+
* A REQUIRED key with a NULLABLE value. NULL = not computed yet: discovery writes it null and the
|
|
89
|
+
* server computes it per source x target when it builds the draft set item. Every consumer reads
|
|
90
|
+
* null as UNRESOLVED (fail closed) — never as 'fits'.
|
|
91
|
+
*/
|
|
92
|
+
audienceFit: z.enum(AUDIENCE_FITS).nullable(),
|
|
93
|
+
/** Machine state: the sensitivity default. NULL until classified — never `excluded` as a stand-in. */
|
|
94
|
+
recommendedUse: z.enum(SOURCE_USES).nullable(),
|
|
95
|
+
/** The COMMITTED treatment: null until a person (or an accepted approval default) commits one. */
|
|
96
|
+
use: z.enum(SOURCE_USES).nullable(),
|
|
97
|
+
/** Provenance of `use`; null exactly when `use` is null (INV-SOURCESYNC-RECOMMENDATION-IS-NOT-COMMITMENT). */
|
|
98
|
+
selectionSource: z.enum(SELECTION_SOURCES).nullable(),
|
|
99
|
+
/** Field-level write barriers a person placed on this line (INV-SOURCESYNC-HOLDS-ARE-FIELD-BARRIERS). */
|
|
100
|
+
personHoldReasons: z.array(z.enum(PERSON_HOLD_REASONS)).max(4),
|
|
101
|
+
/** The line's machine lifecycle (INV-SOURCESYNC-SET-IS-RECONCILED). */
|
|
102
|
+
machineState: z.enum(CANDIDATE_MACHINE_STATES),
|
|
103
|
+
/** Structured reasons for `relevance`; `matchReason` is rendered from these by the server. */
|
|
104
|
+
relevanceReasons: z.array(RelevanceReasonSchema).max(16),
|
|
105
|
+
/** The audience Attach would disclose to. NULL = unknown — reads as MAY WIDEN (INV-SOURCESYNC-ATTACH-SHOWS-ITS-AUDIENCE). */
|
|
106
|
+
targetAudience: z.enum(TARGET_AUDIENCES).nullable(),
|
|
107
|
+
/** Which lane earned each model tier's attention (INV-SOURCESYNC-EXPLORATION-RESERVED). */
|
|
108
|
+
attention: CandidateAttentionSchema,
|
|
109
|
+
/**
|
|
110
|
+
* Whether this line is in the set that approval would land — the SERVER's
|
|
111
|
+
* answer, never derived from `use`. A decoy, a series' history or a line a
|
|
112
|
+
* person left out is false while its `use` is still null.
|
|
113
|
+
*/
|
|
114
|
+
included: z.boolean(),
|
|
115
|
+
/** True while a `widens` item — or one whose fit is not computed yet — has no person-chosen use: approval is refused until none remain. */
|
|
116
|
+
treatmentUnresolved: z.boolean(),
|
|
117
|
+
/**
|
|
118
|
+
* Product-owned copy saying why this file was proposed, rendered by the server from
|
|
119
|
+
* `relevanceReasons`. Never model-authored prose. Deprecated: removed next release.
|
|
120
|
+
*/
|
|
121
|
+
matchReason: z.string().min(1).max(500),
|
|
122
|
+
/** Non-null when the source could not be read (a scanned PDF, an unsupported kind). */
|
|
123
|
+
unreadableReason: z
|
|
124
|
+
.enum(["unsupported_type", "needs_ocr", "unsupported_kind"])
|
|
125
|
+
.nullable(),
|
|
126
|
+
})
|
|
127
|
+
.superRefine(refineCandidateRelevance);
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* One edit to one candidate item: a PATCH names EXACTLY ONE thing that changes, so bringing
|
|
131
|
+
* a line into review never also commits a treatment.
|
|
132
|
+
* The SERVER bumps the set's revision on every accepted edit; the client never sends one here.
|
|
133
|
+
*/
|
|
134
|
+
export const SourceSyncCandidateItemPatchSchema = z
|
|
135
|
+
.object({
|
|
136
|
+
/** Bring into review: hidden → shown. Changes VISIBILITY ONLY (INV-SOURCESYNC-RECOMMENDATION-IS-NOT-COMMITMENT). */
|
|
137
|
+
visibility: z.literal("shown").optional(),
|
|
138
|
+
/** Attach, direction only, or leave out. Choosing one on a `widens` item RESOLVES its treatment. */
|
|
139
|
+
use: z.enum(SOURCE_USES).optional(),
|
|
140
|
+
/** Correct the class the classifier chose. */
|
|
141
|
+
sourceClass: z.enum(SOURCE_CLASSES).optional(),
|
|
142
|
+
/** Re-route to another target document. The server re-computes audienceFit for the new target. */
|
|
143
|
+
targetDocId: z.string().uuid().optional(),
|
|
144
|
+
})
|
|
145
|
+
.strict()
|
|
146
|
+
.refine((patch) => Object.keys(patch).length === 1, {
|
|
147
|
+
message: "a patch changes exactly one thing",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
export type SourceSyncCandidateItem = z.infer<
|
|
151
|
+
typeof SourceSyncCandidateItemSchema
|
|
152
|
+
>;
|
|
153
|
+
export type SourceSyncCandidateItemPatch = z.infer<
|
|
154
|
+
typeof SourceSyncCandidateItemPatchSchema
|
|
155
|
+
>;
|
|
@@ -7,10 +7,8 @@
|
|
|
7
7
|
* with the same function, so no surface counts "files" its own way.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type {
|
|
11
|
-
|
|
12
|
-
SourceSyncCandidateTotals,
|
|
13
|
-
} from "./schemas";
|
|
10
|
+
import type { SourceSyncCandidateItem } from "./candidate-item";
|
|
11
|
+
import type { SourceSyncCandidateTotals } from "./schemas";
|
|
14
12
|
|
|
15
13
|
/** The relevance facts the count reads (control ADR source-discovery-relevance-funnel). */
|
|
16
14
|
type RelevanceFacts = "visibility" | "machineState";
|
package/src/source-sync/index.ts
CHANGED
|
@@ -29,6 +29,8 @@ export type {
|
|
|
29
29
|
SourceSyncDocDisposition,
|
|
30
30
|
SourceSyncScopeApprovalState,
|
|
31
31
|
SourceSyncScopeApproverKind,
|
|
32
|
+
DocumentType,
|
|
33
|
+
KnownDocumentType,
|
|
32
34
|
} from "./vocabulary";
|
|
33
35
|
|
|
34
36
|
export {
|
|
@@ -42,6 +44,7 @@ export {
|
|
|
42
44
|
SOURCE_SYNC_DOC_DISPOSITIONS,
|
|
43
45
|
SOURCE_SYNC_SCOPE_APPROVAL_STATES,
|
|
44
46
|
SOURCE_SYNC_SCOPE_APPROVER_KINDS,
|
|
47
|
+
DOCUMENT_TYPES,
|
|
45
48
|
} from "./vocabulary";
|
|
46
49
|
|
|
47
50
|
export type {
|
|
@@ -84,11 +87,19 @@ export {
|
|
|
84
87
|
} from "./relevance";
|
|
85
88
|
|
|
86
89
|
export type {
|
|
87
|
-
SourceSyncDiscoveryRunResponse,
|
|
88
90
|
SourceSyncCandidateItem,
|
|
91
|
+
SourceSyncCandidateItemPatch,
|
|
92
|
+
} from "./candidate-item";
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
SourceSyncCandidateItemSchema,
|
|
96
|
+
SourceSyncCandidateItemPatchSchema,
|
|
97
|
+
} from "./candidate-item";
|
|
98
|
+
|
|
99
|
+
export type {
|
|
100
|
+
SourceSyncDiscoveryRunResponse,
|
|
89
101
|
SourceSyncCandidateSetResponse,
|
|
90
102
|
SourceSyncCandidateTotals,
|
|
91
|
-
SourceSyncCandidateItemPatch,
|
|
92
103
|
SourceSyncAddCandidateRequest,
|
|
93
104
|
SourceSyncApproveRequest,
|
|
94
105
|
SourceSyncScopeApproval,
|
|
@@ -107,10 +118,8 @@ export type {
|
|
|
107
118
|
export {
|
|
108
119
|
SOURCE_SYNC_RISK_REASONS,
|
|
109
120
|
SourceSyncDiscoveryRunResponseSchema,
|
|
110
|
-
SourceSyncCandidateItemSchema,
|
|
111
121
|
SourceSyncCandidateSetResponseSchema,
|
|
112
122
|
SourceSyncCandidateTotalsSchema,
|
|
113
|
-
SourceSyncCandidateItemPatchSchema,
|
|
114
123
|
SourceSyncAddCandidateRequestSchema,
|
|
115
124
|
SourceSyncApproveRequestSchema,
|
|
116
125
|
SourceSyncScopeApprovalSchema,
|
|
@@ -51,7 +51,11 @@ export const SIGNAL_REASON_CODES = [
|
|
|
51
51
|
] as const;
|
|
52
52
|
export type SignalReasonCode = (typeof SIGNAL_REASON_CODES)[number];
|
|
53
53
|
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* What was NOT evaluated, or was explored, or could not be decided. Never negative evidence
|
|
56
|
+
* (INV-SOURCESYNC-MISSING-EVIDENCE-IS-NOT-EVIDENCE; INV-SOURCESYNC-NONEXECUTION-IS-NOT-NEGATIVE-EVIDENCE):
|
|
57
|
+
* an `execution_failed_*` tier (timeout, budget, provider, parse) reads as uncertainty, never as irrelevance.
|
|
58
|
+
*/
|
|
55
59
|
export const EVALUATION_REASON_CODES = [
|
|
56
60
|
"not_evaluated_t1",
|
|
57
61
|
"not_evaluated_t2",
|
|
@@ -59,14 +63,26 @@ export const EVALUATION_REASON_CODES = [
|
|
|
59
63
|
"folder_truncated",
|
|
60
64
|
"explored_t1",
|
|
61
65
|
"explored_t2",
|
|
66
|
+
"peek_insufficient_evidence",
|
|
67
|
+
"type_disagreement",
|
|
68
|
+
"type_below_confidence",
|
|
69
|
+
"execution_failed_t1",
|
|
70
|
+
"execution_failed_t2",
|
|
62
71
|
] as const;
|
|
63
72
|
export type EvaluationReasonCode = (typeof EVALUATION_REASON_CODES)[number];
|
|
64
73
|
|
|
65
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* A model tier's contribution. `type_policy` carries the POLICY-APPLIED score of the resolved document
|
|
76
|
+
* type (control ADR source-discovery-observation-vs-policy). The peek verdict is domain-named:
|
|
77
|
+
* strategy evidence present / absent. `peek_confirms` / `peek_contradicts` stay for v1-pinned rows.
|
|
78
|
+
*/
|
|
66
79
|
export const MODEL_REASON_CODES = [
|
|
67
80
|
"title_path_score",
|
|
68
81
|
"peek_confirms",
|
|
69
82
|
"peek_contradicts",
|
|
83
|
+
"type_policy",
|
|
84
|
+
"peek_strategy_evidence_present",
|
|
85
|
+
"peek_strategy_evidence_absent",
|
|
70
86
|
] as const;
|
|
71
87
|
export type ModelReasonCode = (typeof MODEL_REASON_CODES)[number];
|
|
72
88
|
|
|
@@ -173,12 +189,16 @@ type CandidateRelevanceFacts = {
|
|
|
173
189
|
sourceClass: string | null;
|
|
174
190
|
use: string | null;
|
|
175
191
|
selectionSource: SelectionSource | null;
|
|
192
|
+
documentType: string | null;
|
|
193
|
+
documentTypeConfidence: number | null;
|
|
176
194
|
};
|
|
177
195
|
|
|
178
196
|
/**
|
|
179
197
|
* The candidate item's cross-field rules (control ADR source-discovery-relevance-funnel):
|
|
180
198
|
* a hidden line is never included; classification decides whether a recommendation and a
|
|
181
|
-
* class exist; `selectionSource` is present exactly when a treatment is committed.
|
|
199
|
+
* class exist; `selectionSource` is present exactly when a treatment is committed. A
|
|
200
|
+
* document type exists only on a classified line, and carries a confidence exactly when
|
|
201
|
+
* it is a known type (control ADR source-discovery-observation-vs-policy).
|
|
182
202
|
*/
|
|
183
203
|
export function refineCandidateRelevance(
|
|
184
204
|
item: CandidateRelevanceFacts,
|
|
@@ -213,4 +233,19 @@ export function refineCandidateRelevance(
|
|
|
213
233
|
message: "selectionSource is present exactly when use is",
|
|
214
234
|
});
|
|
215
235
|
}
|
|
236
|
+
if (item.documentType !== null && unclassified) {
|
|
237
|
+
ctx.addIssue({
|
|
238
|
+
code: "custom",
|
|
239
|
+
message: "an unclassified line has no document type",
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const knownType =
|
|
243
|
+
item.documentType !== null && item.documentType !== "unknown";
|
|
244
|
+
if (knownType !== (item.documentTypeConfidence !== null)) {
|
|
245
|
+
ctx.addIssue({
|
|
246
|
+
code: "custom",
|
|
247
|
+
message:
|
|
248
|
+
"a known document type carries a confidence; unknown and absent carry none",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
216
251
|
}
|