@company-semantics/contracts 58.0.0 → 58.2.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 +4 -4
- package/src/__tests__/resource-keys.test.ts +30 -0
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +33 -1
- package/src/chat/README.md +15 -4
- package/src/chat/__tests__/proactive-kind.test.ts +51 -0
- package/src/chat/index.ts +9 -0
- package/src/chat/proactive-kind.ts +51 -0
- package/src/chat/schemas.ts +92 -1
- package/src/chat/types.ts +19 -1
- package/src/index.ts +110 -0
- package/src/message-parts/README.md +5 -0
- package/src/message-parts/__tests__/suggested-replies.test.ts +52 -0
- package/src/message-parts/__tests__/wire.test.ts +48 -0
- package/src/message-parts/index.ts +8 -0
- package/src/message-parts/suggested-replies.ts +48 -0
- package/src/message-parts/types.ts +7 -1
- package/src/message-parts/wire.ts +26 -0
- package/src/notifications/__tests__/__snapshots__/monospace-budget.test.ts.snap +1 -0
- package/src/notifications/__tests__/__snapshots__/registry.test.ts.snap +1 -0
- package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +207 -0
- package/src/notifications/__tests__/fixtures.ts +9 -0
- package/src/notifications/__tests__/org-invite.test.ts +75 -0
- package/src/notifications/__tests__/render-snapshot.test.ts +8 -0
- package/src/notifications/kinds/org-invite.ts +27 -12
- package/src/notifications/payloads.ts +7 -0
- package/src/org/README.md +38 -0
- package/src/org/__tests__/canonical-facts.test.ts +118 -0
- package/src/org/__tests__/structure-inference.test.ts +392 -0
- package/src/org/__tests__/structure-provenance.test.ts +187 -0
- package/src/org/canonical-facts.ts +94 -1
- package/src/org/index.ts +54 -0
- package/src/org/schemas.ts +23 -0
- package/src/org/structure-inference.ts +521 -0
- package/src/proactive/README.md +125 -0
- package/src/proactive/__tests__/README.md +56 -0
- package/src/proactive/__tests__/chat-templates.test.ts +167 -0
- package/src/proactive/__tests__/compile-fixtures.ts +110 -0
- package/src/proactive/__tests__/vocabulary.test.ts +279 -0
- package/src/proactive/classes.ts +125 -0
- package/src/proactive/composer.ts +104 -0
- package/src/proactive/facts.ts +87 -0
- package/src/proactive/index.ts +52 -0
- package/src/proactive/kinds.ts +127 -0
- package/src/proactive/plan.ts +79 -0
- package/src/proactive/registry.ts +71 -0
- package/src/proactive/surfaces.ts +59 -0
- package/src/proactive/templates/README.md +58 -0
- package/src/proactive/templates/index.ts +32 -0
- package/src/proactive/templates/morning-brief.ts +77 -0
- package/src/proactive/templates/org-became-shared.ts +54 -0
- package/src/resource-key-types.ts +9 -0
- package/src/resource-keys.ts +2 -0
- package/src/user-notifications/README.md +10 -0
- package/src/user-notifications/kinds.ts +28 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
StructureEvidenceSchema,
|
|
5
|
+
TOPOLOGY_ONLY_EVIDENCE_KINDS,
|
|
6
|
+
StructureUnitAuthoritySchema,
|
|
7
|
+
ProposedOrgUnitSchema,
|
|
8
|
+
PersonStructureOutcomeSchema,
|
|
9
|
+
StructureReviewItemSchema,
|
|
10
|
+
StructureProposalSchema,
|
|
11
|
+
StructurePersonFactSchema,
|
|
12
|
+
StructureReportingFactSchema,
|
|
13
|
+
ExistingUnitFactSchema,
|
|
14
|
+
ExistingPlacementFactSchema,
|
|
15
|
+
StructureInferenceSnapshotSchema,
|
|
16
|
+
} from "../structure-inference.js";
|
|
17
|
+
|
|
18
|
+
const UNIT_ID = "11111111-1111-4111-8111-111111111111";
|
|
19
|
+
const ORG_ID = "22222222-2222-4222-8222-222222222222";
|
|
20
|
+
|
|
21
|
+
const roleFamilyEvidence = {
|
|
22
|
+
kind: "role_family" as const,
|
|
23
|
+
family: "Technical Recruiter",
|
|
24
|
+
personIds: ["p1", "p2", "p3", "p4"],
|
|
25
|
+
count: 4,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const topologyEvidence = {
|
|
29
|
+
kind: "reporting_cluster" as const,
|
|
30
|
+
managerPersonId: "mgr-1",
|
|
31
|
+
personIds: ["p1", "p2"],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe("StructureEvidenceSchema", () => {
|
|
35
|
+
it("round-trips every evidence kind the validator reasons over", () => {
|
|
36
|
+
const kinds = [
|
|
37
|
+
{ kind: "explicit_department", personIds: ["p1"], value: "People" },
|
|
38
|
+
{ kind: "explicit_division", personIds: ["p1"], value: "G&A" },
|
|
39
|
+
roleFamilyEvidence,
|
|
40
|
+
{ kind: "functional_leader", personId: "p9", function: "People" },
|
|
41
|
+
topologyEvidence,
|
|
42
|
+
{ kind: "administrative_pivot", personId: "p7", departmentEntropy: 1.9 },
|
|
43
|
+
{ kind: "controlled_ontology", concept: "Recruiting" },
|
|
44
|
+
];
|
|
45
|
+
for (const evidence of kinds) {
|
|
46
|
+
expect(StructureEvidenceSchema.parse(evidence)).toEqual(evidence);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("rejects prose masquerading as evidence — the whole point of typing it", () => {
|
|
51
|
+
expect(() =>
|
|
52
|
+
StructureEvidenceSchema.parse("several employees have similar roles"),
|
|
53
|
+
).toThrow();
|
|
54
|
+
expect(() =>
|
|
55
|
+
StructureEvidenceSchema.parse({ kind: "vibes", note: "feels right" }),
|
|
56
|
+
).toThrow();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("names administrative_pivot, so a proposal can explain a boundary it did NOT draw", () => {
|
|
60
|
+
const parsed = StructureEvidenceSchema.parse({
|
|
61
|
+
kind: "administrative_pivot",
|
|
62
|
+
personId: "olivia",
|
|
63
|
+
departmentEntropy: 1.94,
|
|
64
|
+
});
|
|
65
|
+
expect(parsed.kind).toBe("administrative_pivot");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("TOPOLOGY_ONLY_EVIDENCE_KINDS", () => {
|
|
70
|
+
it("names reporting_cluster and nothing else", () => {
|
|
71
|
+
expect(TOPOLOGY_ONLY_EVIDENCE_KINDS).toEqual(["reporting_cluster"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("holds only kinds StructureEvidence actually declares", () => {
|
|
75
|
+
for (const kind of TOPOLOGY_ONLY_EVIDENCE_KINDS) {
|
|
76
|
+
expect(() =>
|
|
77
|
+
StructureEvidenceSchema.parse({
|
|
78
|
+
kind,
|
|
79
|
+
managerPersonId: "mgr-1",
|
|
80
|
+
personIds: [],
|
|
81
|
+
}),
|
|
82
|
+
).not.toThrow();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("StructureUnitAuthoritySchema", () => {
|
|
88
|
+
it("is the anchor-vs-reorganizable axis, not the fact truth hierarchy", () => {
|
|
89
|
+
expect(StructureUnitAuthoritySchema.options).toEqual([
|
|
90
|
+
"human_confirmed",
|
|
91
|
+
"ai_inferred",
|
|
92
|
+
"hris_source",
|
|
93
|
+
]);
|
|
94
|
+
// The FactSourceTier members must NOT be accepted here — two closed sets
|
|
95
|
+
// meaning almost the same thing is exactly the ambiguity to avoid.
|
|
96
|
+
expect(() => StructureUnitAuthoritySchema.parse("inferred")).toThrow();
|
|
97
|
+
expect(() => StructureUnitAuthoritySchema.parse("user")).toThrow();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("ProposedOrgUnitSchema", () => {
|
|
102
|
+
const base = {
|
|
103
|
+
tempId: "u1",
|
|
104
|
+
targetUnitId: UNIT_ID,
|
|
105
|
+
identityConfidence: 0.95,
|
|
106
|
+
identityEvidence: [roleFamilyEvidence],
|
|
107
|
+
name: "People",
|
|
108
|
+
nameBasis: [roleFamilyEvidence],
|
|
109
|
+
parentTempId: null,
|
|
110
|
+
suggestedTypeLabel: "Department",
|
|
111
|
+
headPersonId: "p9",
|
|
112
|
+
nameConfidence: 0.8,
|
|
113
|
+
structureConfidence: 0.9,
|
|
114
|
+
provisional: false,
|
|
115
|
+
evidence: [roleFamilyEvidence],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
it("carries targetUnitId so a rename preserves durable identity", () => {
|
|
119
|
+
expect(ProposedOrgUnitSchema.parse(base).targetUnitId).toBe(UNIT_ID);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("requires targetUnitId to be present-or-null, never absent", () => {
|
|
123
|
+
const { targetUnitId: _omitted, ...without } = base;
|
|
124
|
+
expect(() => ProposedOrgUnitSchema.parse(without)).toThrow();
|
|
125
|
+
expect(
|
|
126
|
+
ProposedOrgUnitSchema.parse({ ...base, targetUnitId: null }).targetUnitId,
|
|
127
|
+
).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("requires targetUnitId to be a durable uuid, not a name or a tempId", () => {
|
|
131
|
+
expect(() =>
|
|
132
|
+
ProposedOrgUnitSchema.parse({ ...base, targetUnitId: "People" }),
|
|
133
|
+
).toThrow();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("rejects confidences outside [0,1]", () => {
|
|
137
|
+
expect(() =>
|
|
138
|
+
ProposedOrgUnitSchema.parse({ ...base, nameConfidence: 1.4 }),
|
|
139
|
+
).toThrow();
|
|
140
|
+
expect(() =>
|
|
141
|
+
ProposedOrgUnitSchema.parse({ ...base, structureConfidence: -0.1 }),
|
|
142
|
+
).toThrow();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("keeps nameBasis separate from boundary evidence", () => {
|
|
146
|
+
const parsed = ProposedOrgUnitSchema.parse({
|
|
147
|
+
...base,
|
|
148
|
+
nameBasis: [],
|
|
149
|
+
evidence: [topologyEvidence],
|
|
150
|
+
});
|
|
151
|
+
expect(parsed.nameBasis).toEqual([]);
|
|
152
|
+
expect(parsed.evidence).toEqual([topologyEvidence]);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("PersonStructureOutcomeSchema", () => {
|
|
157
|
+
it("round-trips all three outcomes — placed, needs_review, excluded", () => {
|
|
158
|
+
const placed = PersonStructureOutcomeSchema.parse({
|
|
159
|
+
kind: "placed",
|
|
160
|
+
personId: "p1",
|
|
161
|
+
unitTempId: "u1",
|
|
162
|
+
confidence: 0.9,
|
|
163
|
+
evidence: [roleFamilyEvidence],
|
|
164
|
+
conflicts: [],
|
|
165
|
+
});
|
|
166
|
+
expect(placed.kind).toBe("placed");
|
|
167
|
+
|
|
168
|
+
const review = PersonStructureOutcomeSchema.parse({
|
|
169
|
+
kind: "needs_review",
|
|
170
|
+
personId: "emily",
|
|
171
|
+
reason: "insufficient_evidence",
|
|
172
|
+
});
|
|
173
|
+
expect(review.kind).toBe("needs_review");
|
|
174
|
+
|
|
175
|
+
const excluded = PersonStructureOutcomeSchema.parse({
|
|
176
|
+
kind: "excluded",
|
|
177
|
+
personId: "bamboo-svc",
|
|
178
|
+
reason: "service_account",
|
|
179
|
+
});
|
|
180
|
+
expect(excluded.kind).toBe("excluded");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("gives an unplaceable person a REASON — never a guess, never an absence", () => {
|
|
184
|
+
expect(() =>
|
|
185
|
+
PersonStructureOutcomeSchema.parse({
|
|
186
|
+
kind: "needs_review",
|
|
187
|
+
personId: "emily",
|
|
188
|
+
}),
|
|
189
|
+
).toThrow();
|
|
190
|
+
expect(() =>
|
|
191
|
+
PersonStructureOutcomeSchema.parse({
|
|
192
|
+
kind: "needs_review",
|
|
193
|
+
personId: "emily",
|
|
194
|
+
reason: "felt_odd",
|
|
195
|
+
}),
|
|
196
|
+
).toThrow();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("has no 'unplaced' or empty outcome — every person lands in one arm", () => {
|
|
200
|
+
expect(() =>
|
|
201
|
+
PersonStructureOutcomeSchema.parse({ kind: "unplaced", personId: "p1" }),
|
|
202
|
+
).toThrow();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("requires a placed outcome to name the unit it placed the person in", () => {
|
|
206
|
+
expect(() =>
|
|
207
|
+
PersonStructureOutcomeSchema.parse({
|
|
208
|
+
kind: "placed",
|
|
209
|
+
personId: "p1",
|
|
210
|
+
confidence: 0.9,
|
|
211
|
+
evidence: [],
|
|
212
|
+
conflicts: [],
|
|
213
|
+
}),
|
|
214
|
+
).toThrow();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("StructureReviewItemSchema", () => {
|
|
219
|
+
const base = {
|
|
220
|
+
kind: "placement_conflict",
|
|
221
|
+
personIds: ["clarissa"],
|
|
222
|
+
unitTempIds: ["u1", "u2"],
|
|
223
|
+
question: "Which unit does this Account Executive belong to?",
|
|
224
|
+
recommended: "u2",
|
|
225
|
+
alternatives: ["u1"],
|
|
226
|
+
signals: {
|
|
227
|
+
department: "Human Resources",
|
|
228
|
+
manager: "VP of Marketing",
|
|
229
|
+
roleFamily: "Account Executive",
|
|
230
|
+
},
|
|
231
|
+
confidence: 0.45,
|
|
232
|
+
evidence: [roleFamilyEvidence],
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
it("carries the competing signals so a reviewer can see WHY it is ambiguous", () => {
|
|
236
|
+
const parsed = StructureReviewItemSchema.parse(base);
|
|
237
|
+
expect(Object.keys(parsed.signals)).toHaveLength(3);
|
|
238
|
+
expect(parsed.signals.manager).toBe("VP of Marketing");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("rejects an unknown review kind", () => {
|
|
242
|
+
expect(() =>
|
|
243
|
+
StructureReviewItemSchema.parse({ ...base, kind: "something_else" }),
|
|
244
|
+
).toThrow();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("StructureProposalSchema", () => {
|
|
249
|
+
const proposal = {
|
|
250
|
+
orgId: ORG_ID,
|
|
251
|
+
sourceSnapshotId: "snap-1",
|
|
252
|
+
structureRevision: "rev-7",
|
|
253
|
+
inputHash: "sha256:abc",
|
|
254
|
+
promptVersion: "3",
|
|
255
|
+
orgUnits: [
|
|
256
|
+
{
|
|
257
|
+
tempId: "u1",
|
|
258
|
+
targetUnitId: UNIT_ID,
|
|
259
|
+
identityConfidence: 0.95,
|
|
260
|
+
identityEvidence: [roleFamilyEvidence],
|
|
261
|
+
name: "People",
|
|
262
|
+
nameBasis: [roleFamilyEvidence],
|
|
263
|
+
parentTempId: null,
|
|
264
|
+
suggestedTypeLabel: "Department",
|
|
265
|
+
headPersonId: "p9",
|
|
266
|
+
nameConfidence: 0.8,
|
|
267
|
+
structureConfidence: 0.9,
|
|
268
|
+
provisional: false,
|
|
269
|
+
evidence: [roleFamilyEvidence],
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
personOutcomes: [
|
|
273
|
+
{
|
|
274
|
+
kind: "placed",
|
|
275
|
+
personId: "p1",
|
|
276
|
+
unitTempId: "u1",
|
|
277
|
+
confidence: 0.9,
|
|
278
|
+
evidence: [roleFamilyEvidence],
|
|
279
|
+
conflicts: [],
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
structureWarnings: [],
|
|
283
|
+
reviewItems: [],
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
it("round-trips a whole proposal without loss", () => {
|
|
287
|
+
expect(StructureProposalSchema.parse(proposal)).toEqual(proposal);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("keeps inputHash and sourceSnapshotId as separate fields", () => {
|
|
291
|
+
// Staleness is decided by inputHash; sourceSnapshotId is lineage only.
|
|
292
|
+
const parsed = StructureProposalSchema.parse(proposal);
|
|
293
|
+
expect(parsed.inputHash).not.toBe(parsed.sourceSnapshotId);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("rejects an orgId that is not a durable uuid", () => {
|
|
297
|
+
expect(() =>
|
|
298
|
+
StructureProposalSchema.parse({ ...proposal, orgId: "Bamboo Workspace" }),
|
|
299
|
+
).toThrow();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe("StructureInferenceSnapshotSchema", () => {
|
|
304
|
+
const person = {
|
|
305
|
+
personId: "p1",
|
|
306
|
+
sourcePersonId: "emp-42",
|
|
307
|
+
displayName: "Duke Smith",
|
|
308
|
+
title: "Technical Recruiter",
|
|
309
|
+
department: "Human Resources",
|
|
310
|
+
division: null,
|
|
311
|
+
location: null,
|
|
312
|
+
managerPersonId: "mgr-1",
|
|
313
|
+
directReportPersonIds: [],
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const snapshot = {
|
|
317
|
+
orgId: ORG_ID,
|
|
318
|
+
sourceSnapshotId: "snap-1",
|
|
319
|
+
structureRevision: "rev-7",
|
|
320
|
+
persons: [person],
|
|
321
|
+
reporting: [
|
|
322
|
+
{
|
|
323
|
+
reportPersonId: "p1",
|
|
324
|
+
managerPersonId: "mgr-1",
|
|
325
|
+
relationshipType: "solid",
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
existingUnits: [
|
|
329
|
+
{
|
|
330
|
+
unitId: UNIT_ID,
|
|
331
|
+
name: "Human Resources",
|
|
332
|
+
parentUnitId: null,
|
|
333
|
+
depth: 2,
|
|
334
|
+
authority: "hris_source",
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
existingPlacements: [
|
|
338
|
+
{ personId: "p1", unitId: UNIT_ID, authority: "hris_source" },
|
|
339
|
+
],
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
it("round-trips the persisted-graph view the engine reasons over", () => {
|
|
343
|
+
expect(StructureInferenceSnapshotSchema.parse(snapshot)).toEqual(snapshot);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("accepts a person fact with sourcePersonId stripped for the model", () => {
|
|
347
|
+
const { sourcePersonId: _stripped, ...modelFacing } = person;
|
|
348
|
+
expect(
|
|
349
|
+
StructurePersonFactSchema.parse(modelFacing).sourcePersonId,
|
|
350
|
+
).toBeUndefined();
|
|
351
|
+
expect(
|
|
352
|
+
StructurePersonFactSchema.parse({ ...person, sourcePersonId: null })
|
|
353
|
+
.sourcePersonId,
|
|
354
|
+
).toBeNull();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("still requires the opaque personId — the only handle a proposal may use", () => {
|
|
358
|
+
const { personId: _omitted, ...without } = person;
|
|
359
|
+
expect(() => StructurePersonFactSchema.parse(without)).toThrow();
|
|
360
|
+
expect(() =>
|
|
361
|
+
StructurePersonFactSchema.parse({ ...person, personId: "" }),
|
|
362
|
+
).toThrow();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("carries the authority axis on every existing unit and placement", () => {
|
|
366
|
+
expect(() =>
|
|
367
|
+
ExistingUnitFactSchema.parse({
|
|
368
|
+
unitId: UNIT_ID,
|
|
369
|
+
name: "People",
|
|
370
|
+
parentUnitId: null,
|
|
371
|
+
depth: 2,
|
|
372
|
+
}),
|
|
373
|
+
).toThrow();
|
|
374
|
+
expect(
|
|
375
|
+
ExistingPlacementFactSchema.parse({
|
|
376
|
+
personId: "p1",
|
|
377
|
+
unitId: UNIT_ID,
|
|
378
|
+
authority: "human_confirmed",
|
|
379
|
+
}).authority,
|
|
380
|
+
).toBe("human_confirmed");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("keeps reporting a typed edge list, so the validator can prove it unchanged", () => {
|
|
384
|
+
expect(() =>
|
|
385
|
+
StructureReportingFactSchema.parse({
|
|
386
|
+
reportPersonId: "p1",
|
|
387
|
+
managerPersonId: "mgr-1",
|
|
388
|
+
relationshipType: "advisory",
|
|
389
|
+
}),
|
|
390
|
+
).toThrow();
|
|
391
|
+
});
|
|
392
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ExistingPlacementFactSchema,
|
|
5
|
+
ExistingUnitFactSchema,
|
|
6
|
+
StructureProposalOriginSchema,
|
|
7
|
+
StructureAcceptedBySchema,
|
|
8
|
+
StructureAuthoritySchema,
|
|
9
|
+
StructureProvenanceSchema,
|
|
10
|
+
StructureUnitAuthoritySchema,
|
|
11
|
+
mayOverrideAuthority,
|
|
12
|
+
} from "../structure-inference.js";
|
|
13
|
+
import type { StructureAuthority } from "../structure-inference.js";
|
|
14
|
+
|
|
15
|
+
const USER_ID = "33333333-3333-4333-8333-333333333333";
|
|
16
|
+
const UNIT_ID = "44444444-4444-4444-8444-444444444444";
|
|
17
|
+
|
|
18
|
+
/** v1: the AI proposed it and an admin clicked Apply. */
|
|
19
|
+
const aiProposedHumanAccepted = {
|
|
20
|
+
proposalOrigin: "ai_inference" as const,
|
|
21
|
+
acceptedBy: { kind: "user" as const, userId: USER_ID },
|
|
22
|
+
authority: "human_confirmed" as const,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe("StructureProposalOriginSchema", () => {
|
|
26
|
+
it("names the three ways a structural fact can come to be proposed", () => {
|
|
27
|
+
for (const origin of ["ai_inference", "hris_source", "manual"]) {
|
|
28
|
+
expect(StructureProposalOriginSchema.parse(origin)).toBe(origin);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("has no user_override member — that value is what the axes replace", () => {
|
|
33
|
+
expect(() =>
|
|
34
|
+
StructureProposalOriginSchema.parse("user_override"),
|
|
35
|
+
).toThrow();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("StructureAcceptedBySchema", () => {
|
|
40
|
+
it("distinguishes a person signing off from a policy deciding", () => {
|
|
41
|
+
expect(
|
|
42
|
+
StructureAcceptedBySchema.parse({ kind: "user", userId: USER_ID }),
|
|
43
|
+
).toEqual({ kind: "user", userId: USER_ID });
|
|
44
|
+
expect(StructureAcceptedBySchema.parse({ kind: "system_policy" })).toEqual({
|
|
45
|
+
kind: "system_policy",
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("refuses an unattributed acceptance — a null acceptor is not an answer", () => {
|
|
50
|
+
expect(() => StructureAcceptedBySchema.parse(null)).toThrow();
|
|
51
|
+
expect(() => StructureAcceptedBySchema.parse({ kind: "user" })).toThrow();
|
|
52
|
+
expect(() =>
|
|
53
|
+
StructureAcceptedBySchema.parse({ kind: "user", userId: "not-a-uuid" }),
|
|
54
|
+
).toThrow();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("StructureAuthoritySchema", () => {
|
|
59
|
+
it("human confirmed outranks ai inferred outranks hris source", () => {
|
|
60
|
+
const descending: StructureAuthority[] = [
|
|
61
|
+
"human_confirmed",
|
|
62
|
+
"ai_inferred",
|
|
63
|
+
"hris_source",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// Every authority may overwrite itself and everything below it...
|
|
67
|
+
for (let i = 0; i < descending.length; i += 1) {
|
|
68
|
+
for (let j = i; j < descending.length; j += 1) {
|
|
69
|
+
expect(mayOverrideAuthority(descending[i]!, descending[j]!)).toBe(true);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ...and nothing below may overwrite something above it.
|
|
73
|
+
for (let i = 0; i < descending.length; i += 1) {
|
|
74
|
+
for (let j = 0; j < i; j += 1) {
|
|
75
|
+
expect(mayOverrideAuthority(descending[i]!, descending[j]!)).toBe(
|
|
76
|
+
false,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("lets an equal authority correct itself, so a first write is not frozen", () => {
|
|
83
|
+
expect(mayOverrideAuthority("hris_source", "hris_source")).toBe(true);
|
|
84
|
+
expect(mayOverrideAuthority("human_confirmed", "human_confirmed")).toBe(
|
|
85
|
+
true,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("stops an HRIS sync reverting an accepted placement", () => {
|
|
90
|
+
expect(mayOverrideAuthority("hris_source", "human_confirmed")).toBe(false);
|
|
91
|
+
expect(mayOverrideAuthority("hris_source", "ai_inferred")).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("is the same ladder the engine reads, not a second copy of it", () => {
|
|
95
|
+
expect(StructureAuthoritySchema).toBe(StructureUnitAuthoritySchema);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("StructureProvenanceSchema", () => {
|
|
100
|
+
it("records acceptance without erasing how the fact originated", () => {
|
|
101
|
+
const parsed = StructureProvenanceSchema.parse(aiProposedHumanAccepted);
|
|
102
|
+
expect(parsed.authority).toBe("human_confirmed");
|
|
103
|
+
// The origin survives acceptance — this is the whole defect being fixed.
|
|
104
|
+
expect(parsed.proposalOrigin).toBe("ai_inference");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("keeps the axes orthogonal: same origin, different acceptor, different authority", () => {
|
|
108
|
+
const autoAccepted = StructureProvenanceSchema.parse({
|
|
109
|
+
proposalOrigin: "ai_inference",
|
|
110
|
+
acceptedBy: { kind: "system_policy" },
|
|
111
|
+
authority: "ai_inferred",
|
|
112
|
+
});
|
|
113
|
+
expect(autoAccepted.proposalOrigin).toBe(
|
|
114
|
+
aiProposedHumanAccepted.proposalOrigin,
|
|
115
|
+
);
|
|
116
|
+
expect(autoAccepted.authority).not.toBe(aiProposedHumanAccepted.authority);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("round-trips the manual and HRIS rows of the provenance table", () => {
|
|
120
|
+
const manual = {
|
|
121
|
+
proposalOrigin: "manual" as const,
|
|
122
|
+
acceptedBy: { kind: "user" as const, userId: USER_ID },
|
|
123
|
+
authority: "human_confirmed" as const,
|
|
124
|
+
};
|
|
125
|
+
const feed = {
|
|
126
|
+
proposalOrigin: "hris_source" as const,
|
|
127
|
+
acceptedBy: { kind: "system_policy" as const },
|
|
128
|
+
authority: "hris_source" as const,
|
|
129
|
+
};
|
|
130
|
+
expect(StructureProvenanceSchema.parse(manual)).toEqual(manual);
|
|
131
|
+
expect(StructureProvenanceSchema.parse(feed)).toEqual(feed);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("carries its authority onto the existing fact a human accepted", () => {
|
|
135
|
+
const accepted = StructureProvenanceSchema.parse(aiProposedHumanAccepted);
|
|
136
|
+
const placement = ExistingPlacementFactSchema.parse({
|
|
137
|
+
personId: "p1",
|
|
138
|
+
unitId: UNIT_ID,
|
|
139
|
+
authority: accepted.authority,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// A human-accepted placement is an ANCHOR, never a revisitable AI guess —
|
|
143
|
+
// recording it as ai_inferred is the failure this vocabulary exists to close.
|
|
144
|
+
expect(placement.authority).toBe("human_confirmed");
|
|
145
|
+
expect(placement.authority).not.toBe("ai_inferred");
|
|
146
|
+
expect(mayOverrideAuthority("ai_inferred", placement.authority)).toBe(
|
|
147
|
+
false,
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("reserves ai_inferred for acceptance by policy, with nobody in the loop", () => {
|
|
152
|
+
const autoAccepted = StructureProvenanceSchema.parse({
|
|
153
|
+
proposalOrigin: "ai_inference",
|
|
154
|
+
acceptedBy: { kind: "system_policy" },
|
|
155
|
+
authority: "ai_inferred",
|
|
156
|
+
});
|
|
157
|
+
expect(autoAccepted.acceptedBy.kind).toBe("system_policy");
|
|
158
|
+
|
|
159
|
+
const unit = ExistingUnitFactSchema.parse({
|
|
160
|
+
unitId: UNIT_ID,
|
|
161
|
+
name: "Platform",
|
|
162
|
+
parentUnitId: null,
|
|
163
|
+
depth: 0,
|
|
164
|
+
authority: autoAccepted.authority,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Nobody signed off, so the engine may still reorganize it and a later
|
|
168
|
+
// human decision overwrites it.
|
|
169
|
+
expect(unit.authority).toBe("ai_inferred");
|
|
170
|
+
expect(mayOverrideAuthority("human_confirmed", unit.authority)).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("requires all three axes — a partial record cannot answer who signed off", () => {
|
|
174
|
+
expect(() =>
|
|
175
|
+
StructureProvenanceSchema.parse({
|
|
176
|
+
proposalOrigin: "ai_inference",
|
|
177
|
+
authority: "human_confirmed",
|
|
178
|
+
}),
|
|
179
|
+
).toThrow();
|
|
180
|
+
expect(() =>
|
|
181
|
+
StructureProvenanceSchema.parse({
|
|
182
|
+
acceptedBy: { kind: "system_policy" },
|
|
183
|
+
authority: "hris_source",
|
|
184
|
+
}),
|
|
185
|
+
).toThrow();
|
|
186
|
+
});
|
|
187
|
+
});
|