@company-semantics/contracts 89.2.0 → 91.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.
Files changed (53) hide show
  1. package/package.json +4 -3
  2. package/src/affordances/README.md +65 -0
  3. package/src/affordances/__tests__/README.md +30 -0
  4. package/src/affordances/__tests__/affordances.test.ts +251 -0
  5. package/src/affordances/affordance.ts +124 -0
  6. package/src/affordances/args.ts +59 -0
  7. package/src/affordances/availability.ts +78 -0
  8. package/src/affordances/ids.ts +45 -0
  9. package/src/affordances/index.ts +39 -0
  10. package/src/api/generated-spec-hash.ts +2 -2
  11. package/src/api/generated.ts +142 -2
  12. package/src/generated/openapi-routes.ts +1 -0
  13. package/src/proactive/README.md +26 -4
  14. package/src/proactive/__tests__/README.md +10 -0
  15. package/src/proactive/__tests__/chat-templates.test.ts +8 -0
  16. package/src/proactive/__tests__/vocabulary.test.ts +11 -0
  17. package/src/proactive/__tests__/workflow-attention-fixtures.ts +49 -0
  18. package/src/proactive/__tests__/workflow-attention.test.ts +146 -0
  19. package/src/proactive/composer.ts +8 -0
  20. package/src/proactive/facts.ts +36 -0
  21. package/src/proactive/index.ts +2 -0
  22. package/src/proactive/kinds.ts +29 -0
  23. package/src/proactive/registry.ts +22 -0
  24. package/src/proactive/surfaces.ts +2 -0
  25. package/src/proactive/templates/README.md +9 -1
  26. package/src/proactive/templates/index.ts +2 -0
  27. package/src/proactive/templates/workflow-needs-attention.ts +35 -0
  28. package/src/root-exports/README.md +1 -0
  29. package/src/root-exports/delivery.ts +69 -1
  30. package/src/source-sync/README.md +95 -5
  31. package/src/source-sync/__tests__/attention-evidence.test.ts +96 -0
  32. package/src/source-sync/__tests__/barrel.test.ts +39 -0
  33. package/src/source-sync/__tests__/enumeration.test.ts +160 -0
  34. package/src/source-sync/__tests__/schemas.test.ts +223 -0
  35. package/src/source-sync/__tests__/vocabulary.test.ts +104 -5
  36. package/src/source-sync/attention-evidence.ts +93 -0
  37. package/src/source-sync/candidate-item.ts +25 -0
  38. package/src/source-sync/enumeration.ts +152 -0
  39. package/src/source-sync/index.ts +35 -1
  40. package/src/source-sync/relevance.ts +85 -11
  41. package/src/source-sync/vocabulary.ts +37 -1
  42. package/src/user-notifications/README.md +8 -0
  43. package/src/user-notifications/kinds.ts +13 -0
  44. package/src/user-notifications/lifecycle-registry.ts +10 -0
  45. package/src/workflows/README.md +38 -2
  46. package/src/workflows/__tests__/README.md +21 -2
  47. package/src/workflows/__tests__/affordances-of-outcome.test.ts +99 -0
  48. package/src/workflows/__tests__/deferred-route.test.ts +168 -0
  49. package/src/workflows/__tests__/outcome.test.ts +66 -0
  50. package/src/workflows/affordances-of-outcome.ts +65 -0
  51. package/src/workflows/deferred-route.ts +240 -0
  52. package/src/workflows/index.ts +34 -0
  53. package/src/workflows/outcome.ts +114 -1
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Index-first enumeration vocabulary tests.
3
+ *
4
+ * INVARIANTS TESTED:
5
+ * - Root statuses combine so that only every root complete is `complete`, and
6
+ * our ceiling plus the provider's incompleteness is `both`
7
+ * - A complete root skips nothing; a root id names exactly a shared drive
8
+ * - A skipped range never ends before it starts
9
+ * - Observed modified times are present exactly when a root enumerated a file
10
+ * - Stage counters are non-negative and narrow from enumerated to shown
11
+ */
12
+
13
+ import { describe, it, expect } from "vitest";
14
+
15
+ import {
16
+ combineEnumerationCompleteness,
17
+ DiscoveryStageCountersSchema,
18
+ EnumerationSkippedRangeSchema,
19
+ RootCoverageSchema,
20
+ } from "../enumeration";
21
+
22
+ function root(overrides: Record<string, unknown> = {}) {
23
+ return {
24
+ rootKind: "user",
25
+ rootId: null,
26
+ status: "complete",
27
+ enumeratedCount: 3,
28
+ observedOldestModifiedTime: "2025-01-01T00:00:00.000Z",
29
+ observedNewestModifiedTime: "2026-09-30T00:00:00.000Z",
30
+ skippedRanges: [],
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ function counters(overrides: Record<string, unknown> = {}) {
36
+ return {
37
+ filesEnumerated: 1000,
38
+ filesMetadataEligible: 800,
39
+ filesT0Scored: 600,
40
+ filesT1Attended: 120,
41
+ filesPeeked: 40,
42
+ filesClassified: 200,
43
+ filesShown: 25,
44
+ filesRetained: 30,
45
+ ...overrides,
46
+ };
47
+ }
48
+
49
+ const RANGE = {
50
+ from: "2024-01-01T00:00:00.000Z",
51
+ to: "2024-06-30T00:00:00.000Z",
52
+ };
53
+
54
+ describe("enumeration", () => {
55
+ it("combines root statuses into one enumeration completeness", () => {
56
+ expect(combineEnumerationCompleteness(["complete", "complete"])).toBe(
57
+ "complete",
58
+ );
59
+ expect(
60
+ combineEnumerationCompleteness(["complete", "local_budget_truncated"]),
61
+ ).toBe("local_budget_truncated");
62
+ expect(
63
+ combineEnumerationCompleteness(["provider_incomplete", "complete"]),
64
+ ).toBe("provider_incomplete");
65
+ expect(
66
+ combineEnumerationCompleteness([
67
+ "local_budget_truncated",
68
+ "provider_incomplete",
69
+ ]),
70
+ ).toBe("both");
71
+ expect(combineEnumerationCompleteness(["both"])).toBe("both");
72
+ });
73
+
74
+ it("a complete root skips nothing", () => {
75
+ expect(RootCoverageSchema.safeParse(root()).success).toBe(true);
76
+ expect(
77
+ RootCoverageSchema.safeParse(root({ skippedRanges: [RANGE] })).success,
78
+ ).toBe(false);
79
+ expect(
80
+ RootCoverageSchema.safeParse(
81
+ root({ status: "local_budget_truncated", skippedRanges: [RANGE] }),
82
+ ).success,
83
+ ).toBe(true);
84
+ });
85
+
86
+ it("a root id names exactly a shared drive", () => {
87
+ expect(
88
+ RootCoverageSchema.safeParse(root({ rootId: "0AbCdEf" })).success,
89
+ ).toBe(false);
90
+ expect(
91
+ RootCoverageSchema.safeParse(root({ rootKind: "shared_drive" })).success,
92
+ ).toBe(false);
93
+ expect(
94
+ RootCoverageSchema.safeParse(
95
+ root({ rootKind: "shared_drive", rootId: "0AbCdEf" }),
96
+ ).success,
97
+ ).toBe(true);
98
+ expect(
99
+ RootCoverageSchema.safeParse(root({ rootKind: "domain" })).success,
100
+ ).toBe(true);
101
+ });
102
+
103
+ it("refuses a skipped range that ends before it starts", () => {
104
+ expect(EnumerationSkippedRangeSchema.safeParse(RANGE).success).toBe(true);
105
+ expect(
106
+ EnumerationSkippedRangeSchema.safeParse({
107
+ from: RANGE.to,
108
+ to: RANGE.from,
109
+ }).success,
110
+ ).toBe(false);
111
+ });
112
+
113
+ it("an empty root has no observed modified times", () => {
114
+ expect(
115
+ RootCoverageSchema.safeParse(root({ enumeratedCount: 0 })).success,
116
+ ).toBe(false);
117
+ expect(
118
+ RootCoverageSchema.safeParse(
119
+ root({
120
+ enumeratedCount: 0,
121
+ observedOldestModifiedTime: null,
122
+ observedNewestModifiedTime: null,
123
+ }),
124
+ ).success,
125
+ ).toBe(true);
126
+ expect(
127
+ RootCoverageSchema.safeParse(root({ observedNewestModifiedTime: null }))
128
+ .success,
129
+ ).toBe(false);
130
+ expect(
131
+ RootCoverageSchema.safeParse(
132
+ root({
133
+ observedOldestModifiedTime: "2026-09-30T00:00:00.000Z",
134
+ observedNewestModifiedTime: "2025-01-01T00:00:00.000Z",
135
+ }),
136
+ ).success,
137
+ ).toBe(false);
138
+ });
139
+
140
+ it("stage counters narrow from enumerated to shown", () => {
141
+ expect(DiscoveryStageCountersSchema.safeParse(counters()).success).toBe(
142
+ true,
143
+ );
144
+ expect(
145
+ DiscoveryStageCountersSchema.safeParse(counters({ filesPeeked: 121 }))
146
+ .success,
147
+ ).toBe(false);
148
+ expect(
149
+ DiscoveryStageCountersSchema.safeParse(counters({ filesShown: 31 }))
150
+ .success,
151
+ ).toBe(false);
152
+ });
153
+
154
+ it("refuses negative stage counters", () => {
155
+ expect(
156
+ DiscoveryStageCountersSchema.safeParse(counters({ filesEnumerated: -1 }))
157
+ .success,
158
+ ).toBe(false);
159
+ });
160
+ });
@@ -21,6 +21,13 @@
21
21
  * - A candidate patch changes exactly one thing: bringing a line into review never
22
22
  * also commits a treatment, and a patch can never hide a line.
23
23
  * - The set response's discovery block is required and null before any completed run.
24
+ * - Index-first discovery (control ADR index-first-operating-knowledge-discovery): an
25
+ * item's operating-knowledge family, canonical path and attention evidence are
26
+ * REQUIRED nullable keys; a family needs a known document type; evidence and a path
27
+ * exist only on a line T1 attended and never on a withheld line.
28
+ * - The discovery block's enumeration fields are all present or all absent, and the
29
+ * run's completeness is the combination of its roots' — a truncated run never
30
+ * claims `complete`.
24
31
  */
25
32
 
26
33
  import { describe, expect, it } from "vitest";
@@ -59,6 +66,8 @@ function candidateItem(overrides: Record<string, unknown> = {}) {
59
66
  sourceClass: "board_deck",
60
67
  documentType: null,
61
68
  documentTypeConfidence: null,
69
+ operatingKnowledgeFamily: null,
70
+ canonicalPath: null,
62
71
  periodLabel: "Q3 2026",
63
72
  seriesRole: "current",
64
73
  relevance: 0.92,
@@ -74,6 +83,7 @@ function candidateItem(overrides: Record<string, unknown> = {}) {
74
83
  relevanceReasons: [],
75
84
  targetAudience: "org_wide",
76
85
  attention: { t1: null, t2: null },
86
+ attentionEvidence: null,
77
87
  included: true,
78
88
  treatmentUnresolved: false,
79
89
  matchReason: "A board deck for the current quarter.",
@@ -143,6 +153,9 @@ function candidateSet(overrides: Record<string, unknown> = {}) {
143
153
  filesDropped: 240,
144
154
  coveragePercent: 87,
145
155
  scanOutcome: "complete",
156
+ enumerationCompleteness: null,
157
+ rootCoverage: null,
158
+ stageCounters: null,
146
159
  },
147
160
  ...overrides,
148
161
  };
@@ -575,6 +588,111 @@ describe("candidate relevance", () => {
575
588
  });
576
589
  });
577
590
 
591
+ describe("index-first discovery item fields", () => {
592
+ const evidence = [
593
+ {
594
+ kind: "path_terms",
595
+ values: ["playbook"],
596
+ source: { producer: "lexicon", version: "3" },
597
+ },
598
+ ];
599
+
600
+ it("an item carries its operating-knowledge family, canonical path and attention evidence", () => {
601
+ const parsed = SourceSyncCandidateItemSchema.safeParse(
602
+ candidateItem({
603
+ documentType: "process_playbook",
604
+ documentTypeConfidence: 0.88,
605
+ operatingKnowledgeFamily: "practices",
606
+ canonicalPath: "Product/How we work",
607
+ attention: { t1: "exploit", t2: null },
608
+ attentionEvidence: evidence,
609
+ }),
610
+ );
611
+ expect(parsed.success).toBe(true);
612
+ expect(parsed.data?.operatingKnowledgeFamily).toBe("practices");
613
+ expect(parsed.data?.canonicalPath).toBe("Product/How we work");
614
+ expect(parsed.data?.attentionEvidence).toEqual(evidence);
615
+ });
616
+
617
+ it("an item from before index-first discovery has no family, no path and no evidence", () => {
618
+ const parsed = SourceSyncCandidateItemSchema.safeParse(candidateItem());
619
+ expect(parsed.success).toBe(true);
620
+ expect(parsed.data?.operatingKnowledgeFamily).toBeNull();
621
+ expect(parsed.data?.canonicalPath).toBeNull();
622
+ expect(parsed.data?.attentionEvidence).toBeNull();
623
+ for (const key of [
624
+ "operatingKnowledgeFamily",
625
+ "canonicalPath",
626
+ "attentionEvidence",
627
+ ]) {
628
+ const item: Record<string, unknown> = candidateItem();
629
+ delete item[key];
630
+ expect(SourceSyncCandidateItemSchema.safeParse(item).success).toBe(false);
631
+ }
632
+ });
633
+
634
+ it("an operating-knowledge family needs a known document type", () => {
635
+ for (const [documentType, documentTypeConfidence] of [
636
+ [null, null],
637
+ ["unknown", null],
638
+ ] as const) {
639
+ const parsed = SourceSyncCandidateItemSchema.safeParse(
640
+ candidateItem({
641
+ documentType,
642
+ documentTypeConfidence,
643
+ operatingKnowledgeFamily: "practices",
644
+ }),
645
+ );
646
+ expect(parsed.success).toBe(false);
647
+ expect(parsed.error?.issues[0]?.message).toBe(
648
+ "an operating-knowledge family needs a known document type",
649
+ );
650
+ }
651
+ });
652
+
653
+ it("a withheld line carries no attention evidence and no path", () => {
654
+ for (const claim of [
655
+ { attentionEvidence: evidence },
656
+ { canonicalPath: "Finance/Board" },
657
+ ]) {
658
+ const parsed = SourceSyncCandidateItemSchema.safeParse(
659
+ candidateItem({
660
+ withheld: true,
661
+ attention: { t1: "exploit", t2: null },
662
+ ...claim,
663
+ }),
664
+ );
665
+ expect(parsed.success).toBe(false);
666
+ expect(parsed.error?.issues[0]?.message).toBe(
667
+ "a withheld line carries no attention evidence and no path",
668
+ );
669
+ }
670
+ });
671
+
672
+ it("attention evidence exists only on a line T1 attended", () => {
673
+ const parsed = SourceSyncCandidateItemSchema.safeParse(
674
+ candidateItem({
675
+ attention: { t1: null, t2: "explore" },
676
+ attentionEvidence: evidence,
677
+ }),
678
+ );
679
+ expect(parsed.success).toBe(false);
680
+ expect(parsed.error?.issues[0]?.message).toBe(
681
+ "attention evidence exists only on a line T1 attended",
682
+ );
683
+ });
684
+
685
+ it("a canonical path exists only on a line T1 attended", () => {
686
+ const parsed = SourceSyncCandidateItemSchema.safeParse(
687
+ candidateItem({ canonicalPath: "Product/How we work" }),
688
+ );
689
+ expect(parsed.success).toBe(false);
690
+ expect(parsed.error?.issues[0]?.message).toBe(
691
+ "a canonical path exists only on a line T1 attended",
692
+ );
693
+ });
694
+ });
695
+
578
696
  describe("SourceSyncCandidateItemPatchSchema", () => {
579
697
  it("bring into review is a patch of visibility alone", () => {
580
698
  expect(
@@ -631,6 +749,111 @@ describe("SourceSyncCandidateSetResponseSchema discovery", () => {
631
749
  ).toBe(false);
632
750
  }
633
751
  });
752
+
753
+ const rootEntry = (status: string, rootId: string | null = null) => ({
754
+ rootKind: rootId === null ? "user" : "shared_drive",
755
+ rootId,
756
+ status,
757
+ enumeratedCount: 3,
758
+ observedOldestModifiedTime: "2024-01-01T00:00:00Z",
759
+ observedNewestModifiedTime: "2026-09-01T00:00:00Z",
760
+ skippedRanges: [],
761
+ });
762
+ const stageCounters = {
763
+ filesEnumerated: 100000,
764
+ filesMetadataEligible: 60000,
765
+ filesT0Scored: 60000,
766
+ filesT1Attended: 400,
767
+ filesPeeked: 120,
768
+ filesClassified: 300,
769
+ filesShown: 12,
770
+ filesRetained: 40,
771
+ };
772
+ const withEnumeration = (enumeration: Record<string, unknown>) =>
773
+ candidateSet({
774
+ discovery: { ...candidateSet().discovery, ...enumeration },
775
+ });
776
+
777
+ it("a discovery block from before index-first discovery has no enumeration fields", () => {
778
+ const parsed =
779
+ SourceSyncCandidateSetResponseSchema.safeParse(candidateSet());
780
+ expect(parsed.success).toBe(true);
781
+ expect(parsed.data?.discovery?.enumerationCompleteness).toBeNull();
782
+ expect(parsed.data?.discovery?.rootCoverage).toBeNull();
783
+ expect(parsed.data?.discovery?.stageCounters).toBeNull();
784
+ for (const key of [
785
+ "enumerationCompleteness",
786
+ "rootCoverage",
787
+ "stageCounters",
788
+ ]) {
789
+ const discovery: Record<string, unknown> = {
790
+ ...candidateSet().discovery,
791
+ };
792
+ delete discovery[key];
793
+ expect(
794
+ SourceSyncCandidateSetResponseSchema.safeParse(
795
+ candidateSet({ discovery }),
796
+ ).success,
797
+ ).toBe(false);
798
+ }
799
+ });
800
+
801
+ it("enumeration fields are all present or all absent", () => {
802
+ expect(
803
+ SourceSyncCandidateSetResponseSchema.safeParse(
804
+ withEnumeration({
805
+ enumerationCompleteness: "complete",
806
+ rootCoverage: [rootEntry("complete")],
807
+ stageCounters,
808
+ }),
809
+ ).success,
810
+ ).toBe(true);
811
+ for (const partial of [
812
+ { enumerationCompleteness: "complete" },
813
+ { enumerationCompleteness: "complete", stageCounters },
814
+ { rootCoverage: [rootEntry("complete")], stageCounters },
815
+ ]) {
816
+ const parsed = SourceSyncCandidateSetResponseSchema.safeParse(
817
+ withEnumeration(partial),
818
+ );
819
+ expect(parsed.success).toBe(false);
820
+ expect(parsed.error?.issues[0]?.message).toBe(
821
+ "enumeration fields are all present or all absent",
822
+ );
823
+ }
824
+ });
825
+
826
+ it("enumeration completeness is the combination of its roots", () => {
827
+ const rootCoverage = [
828
+ rootEntry("complete"),
829
+ rootEntry("local_budget_truncated", "0AbCdEfGhIjK"),
830
+ ];
831
+ const claimsComplete = SourceSyncCandidateSetResponseSchema.safeParse(
832
+ withEnumeration({
833
+ enumerationCompleteness: "complete",
834
+ rootCoverage,
835
+ stageCounters,
836
+ }),
837
+ );
838
+ expect(claimsComplete.success).toBe(false);
839
+ expect(claimsComplete.error?.issues[0]?.message).toBe(
840
+ "enumeration completeness is the combination of its roots",
841
+ );
842
+ const truncated = SourceSyncCandidateSetResponseSchema.safeParse(
843
+ withEnumeration({
844
+ enumerationCompleteness: "local_budget_truncated",
845
+ rootCoverage,
846
+ stageCounters,
847
+ }),
848
+ );
849
+ expect(truncated.success).toBe(true);
850
+ expect(truncated.data?.discovery?.stageCounters?.filesEnumerated).toBe(
851
+ 100000,
852
+ );
853
+ expect(truncated.data?.discovery?.enumerationCompleteness).not.toBe(
854
+ "complete",
855
+ );
856
+ });
634
857
  });
635
858
 
636
859
  describe("SourceSyncDiscoveryRunResponseSchema", () => {
@@ -6,18 +6,31 @@
6
6
  * order.
7
7
  *
8
8
  * INVARIANTS TESTED:
9
- * - Each of the twenty-three closed sets (ten Source Sync, thirteen discovery
10
- * relevance) has exactly the members this release fixes
9
+ * - Each of the twenty-five closed sets (eleven Source Sync, thirteen discovery
10
+ * relevance, one attention evidence) has exactly the members this release
11
+ * fixes
11
12
  * - `decoy` is a source class; `unknown` is a sharing breadth
13
+ * - OPERATING_KNOWLEDGE_FAMILIES names exactly the five families
12
14
  * - No member string appears in two sets
13
15
  * - The four relevance reason code sets are pairwise disjoint
14
- * - DOCUMENT_TYPES has exactly its thirty members, `unknown` last; it sits
16
+ * - DOCUMENT_TYPES has exactly its forty members, the ten operating-knowledge
17
+ * types immediately before `unknown`, `unknown` last; it sits
15
18
  * OUTSIDE the disjointness table and shares a word with another set only
16
19
  * where pinned (a structural name on a separate axis)
20
+ * - ENUMERATION_COMPLETENESS and ENUMERATION_ROOT_KINDS sit OUTSIDE the
21
+ * disjointness table: `complete` is also a scan outcome and `domain` a
22
+ * sharing breadth (same word, separate axis), pinned exactly
17
23
  */
18
24
 
19
25
  import { describe, it, expect } from "vitest";
20
26
 
27
+ import { ATTENTION_SIGNAL_KINDS } from "../attention-evidence";
28
+
29
+ import {
30
+ ENUMERATION_COMPLETENESS,
31
+ ENUMERATION_ROOT_KINDS,
32
+ } from "../enumeration";
33
+
21
34
  import {
22
35
  ATTENTION_LANES,
23
36
  CANDIDATE_MACHINE_STATES,
@@ -37,6 +50,7 @@ import {
37
50
  import {
38
51
  AUDIENCE_FITS,
39
52
  DOCUMENT_TYPES,
53
+ OPERATING_KNOWLEDGE_FAMILIES,
40
54
  SHARING_BREADTHS,
41
55
  SOURCE_CLASSES,
42
56
  SOURCE_SENSITIVITY_CLASSES,
@@ -126,6 +140,17 @@ const VOCABULARIES = [
126
140
  tuple: SOURCE_SYNC_SCOPE_APPROVER_KINDS,
127
141
  members: ["org_admin", "unit_head"],
128
142
  },
143
+ {
144
+ name: "OPERATING_KNOWLEDGE_FAMILIES",
145
+ tuple: OPERATING_KNOWLEDGE_FAMILIES,
146
+ members: [
147
+ "direction",
148
+ "operating_model",
149
+ "practices",
150
+ "identity_market",
151
+ "decisions_learning",
152
+ ],
153
+ },
129
154
  {
130
155
  name: "SOURCE_RELEVANCE_TIERS",
131
156
  tuple: SOURCE_RELEVANCE_TIERS,
@@ -240,6 +265,18 @@ const VOCABULARIES = [
240
265
  tuple: SCAN_OUTCOMES,
241
266
  members: ["complete", "budget_exhausted", "file_ceiling", "stopped"],
242
267
  },
268
+ {
269
+ name: "ATTENTION_SIGNAL_KINDS",
270
+ tuple: ATTENTION_SIGNAL_KINDS,
271
+ members: [
272
+ "tree_prior",
273
+ "path_terms",
274
+ "content_match",
275
+ "breadth",
276
+ "recency",
277
+ "mime_prior",
278
+ ],
279
+ },
243
280
  ] as const satisfies readonly {
244
281
  name: string;
245
282
  tuple: readonly string[];
@@ -274,6 +311,16 @@ describe("Source Sync closed sets", () => {
274
311
  expect(SHARING_BREADTHS).toContain("unknown");
275
312
  });
276
313
 
314
+ it("names the five operating-knowledge families", () => {
315
+ expect([...OPERATING_KNOWLEDGE_FAMILIES]).toEqual([
316
+ "direction",
317
+ "operating_model",
318
+ "practices",
319
+ "identity_market",
320
+ "decisions_learning",
321
+ ]);
322
+ });
323
+
277
324
  it("no member appears in two sets", () => {
278
325
  const all = VOCABULARIES.flatMap(({ tuple }) => [...tuple]);
279
326
  expect(new Set(all).size).toBe(all.length);
@@ -295,7 +342,7 @@ describe("Source Sync closed sets", () => {
295
342
  });
296
343
 
297
344
  describe("document types", () => {
298
- it("has exactly the thirty document types, in order", () => {
345
+ it("has exactly the forty document types, in order", () => {
299
346
  expect([...DOCUMENT_TYPES]).toEqual([
300
347
  "board_deck",
301
348
  "board_minutes",
@@ -326,9 +373,34 @@ describe("document types", () => {
326
373
  "coursework",
327
374
  "empty_template",
328
375
  "image_or_screenshot",
376
+ "team_charter",
377
+ "process_playbook",
378
+ "policy_handbook",
379
+ "onboarding_guide",
380
+ "values_culture",
381
+ "positioning_messaging",
382
+ "decision_log",
383
+ "postmortem",
384
+ "metrics_definition",
385
+ "operating_cadence",
329
386
  "unknown",
330
387
  ]);
331
- expect(new Set(DOCUMENT_TYPES).size).toBe(30);
388
+ expect(new Set(DOCUMENT_TYPES).size).toBe(40);
389
+ });
390
+
391
+ it("the ten operating-knowledge document types sit before unknown", () => {
392
+ expect(DOCUMENT_TYPES.slice(29, 39)).toEqual([
393
+ "team_charter",
394
+ "process_playbook",
395
+ "policy_handbook",
396
+ "onboarding_guide",
397
+ "values_culture",
398
+ "positioning_messaging",
399
+ "decision_log",
400
+ "postmortem",
401
+ "metrics_definition",
402
+ "operating_cadence",
403
+ ]);
332
404
  });
333
405
 
334
406
  it("unknown is the last document type", () => {
@@ -349,3 +421,30 @@ describe("document types", () => {
349
421
  ]);
350
422
  });
351
423
  });
424
+
425
+ describe("enumeration vocabulary", () => {
426
+ it("enumeration vocabularies share a member with another set only where pinned", () => {
427
+ const others = new Set<string>(
428
+ VOCABULARIES.flatMap(({ tuple }) => [...tuple]),
429
+ );
430
+ // SCAN_OUTCOMES
431
+ expect(ENUMERATION_COMPLETENESS.filter((m) => others.has(m))).toEqual([
432
+ "complete",
433
+ ]);
434
+ // SHARING_BREADTHS
435
+ expect(ENUMERATION_ROOT_KINDS.filter((m) => others.has(m))).toEqual([
436
+ "domain",
437
+ ]);
438
+ expect([...ENUMERATION_COMPLETENESS]).toEqual([
439
+ "complete",
440
+ "local_budget_truncated",
441
+ "provider_incomplete",
442
+ "both",
443
+ ]);
444
+ expect([...ENUMERATION_ROOT_KINDS]).toEqual([
445
+ "user",
446
+ "shared_drive",
447
+ "domain",
448
+ ]);
449
+ });
450
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * WHY a file received T1 attention — the evidence vector, each signal with its
3
+ * PROVENANCE (control ADR index-first-operating-knowledge-discovery;
4
+ * INV-SOURCESYNC-ATTENTION-EVIDENCE-HAS-PROVENANCE).
5
+ *
6
+ * It exists only for a file that crossed T1 attention: pre-attention signals are
7
+ * ephemeral (INV-SOURCESYNC-PRE-ATTENTION-SIGNALS-ARE-EPHEMERAL), so a file that never
8
+ * reached attention carries NULL evidence. Every signal names the producer and version
9
+ * that judged it, so "why did c_S find this?" stays answerable after prompts and
10
+ * lexicons change. `producer` is a documented key string (not a closed set, so a new
11
+ * producer needs no contracts major); the producers in use are `lexicon`,
12
+ * `source-discovery-tree-triage`, `source-discovery-plan` and `metadata-scorer`.
13
+ *
14
+ * TERMS are product- or plan-proposed vocabulary that MATCHED — never a slice of the
15
+ * file's name and never text read from the file. A folder is named by its provider
16
+ * id (`folderId`), never its name (INV-SOURCESYNC-TOPOLOGY-IS-NOT-SOURCE-EVIDENCE).
17
+ */
18
+ import { z } from "zod";
19
+ import { SHARING_BREADTHS } from "./vocabulary";
20
+
21
+ export const ATTENTION_SIGNAL_KINDS = [
22
+ "tree_prior",
23
+ "path_terms",
24
+ "content_match",
25
+ "breadth",
26
+ "recency",
27
+ "mime_prior",
28
+ ] as const;
29
+ export type AttentionSignalKind = (typeof ATTENTION_SIGNAL_KINDS)[number];
30
+
31
+ /** Who judged a signal: a producer key (a prompt key, `lexicon`, a scorer) and its version. */
32
+ export const AttentionSignalSourceSchema = z.object({
33
+ producer: z.string().regex(/^[a-z0-9][a-z0-9.-]{0,63}$/),
34
+ version: z.string().min(1).max(32),
35
+ });
36
+ export type AttentionSignalSource = z.infer<typeof AttentionSignalSourceSchema>;
37
+
38
+ const Score = z.number().min(0).max(1);
39
+ const Terms = z.array(z.string().min(1).max(64)).min(1).max(16);
40
+
41
+ export const AttentionSignalSchema = z.discriminatedUnion("kind", [
42
+ z.object({
43
+ kind: z.literal("tree_prior"),
44
+ score: Score,
45
+ /** The provider id of the folder the tree triage ranked — never its name. */
46
+ folderId: z.string().min(1).max(128),
47
+ source: AttentionSignalSourceSchema,
48
+ }),
49
+ z.object({
50
+ kind: z.literal("path_terms"),
51
+ values: Terms,
52
+ source: AttentionSignalSourceSchema,
53
+ }),
54
+ z.object({
55
+ kind: z.literal("content_match"),
56
+ values: Terms,
57
+ strength: Score,
58
+ /** The structured content probe that matched. */
59
+ probeId: z.string().min(1).max(64),
60
+ source: AttentionSignalSourceSchema,
61
+ }),
62
+ z.object({
63
+ kind: z.literal("breadth"),
64
+ value: z.enum(SHARING_BREADTHS),
65
+ source: AttentionSignalSourceSchema,
66
+ }),
67
+ z.object({
68
+ kind: z.literal("recency"),
69
+ score: Score,
70
+ source: AttentionSignalSourceSchema,
71
+ }),
72
+ z.object({
73
+ kind: z.literal("mime_prior"),
74
+ score: Score,
75
+ source: AttentionSignalSourceSchema,
76
+ }),
77
+ ]);
78
+ export type AttentionSignal = z.infer<typeof AttentionSignalSchema>;
79
+
80
+ /** At most one signal of each kind. */
81
+ export const AttentionEvidenceSchema = z
82
+ .array(AttentionSignalSchema)
83
+ .max(ATTENTION_SIGNAL_KINDS.length)
84
+ .superRefine((signals, ctx) => {
85
+ const kinds = signals.map((s) => s.kind);
86
+ if (new Set(kinds).size !== kinds.length) {
87
+ ctx.addIssue({
88
+ code: "custom",
89
+ message: "an attention signal kind appears at most once",
90
+ });
91
+ }
92
+ });
93
+ export type AttentionEvidence = z.infer<typeof AttentionEvidenceSchema>;