@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
package/package.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
|
|
2
|
-
export const SPEC_HASH = '
|
|
3
|
-
export const SPEC_HASH_FULL = '
|
|
2
|
+
export const SPEC_HASH = '53f462b4c90b' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '53f462b4c90ba419e71d55d82e19bb6a9836d95aeaaae6299b8124ac340320d8' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -8876,7 +8876,10 @@ export interface components {
|
|
|
8876
8876
|
targetDocId: string;
|
|
8877
8877
|
targetDocTitle: string;
|
|
8878
8878
|
/** @enum {string} */
|
|
8879
|
-
|
|
8879
|
+
visibility: "hidden" | "shown";
|
|
8880
|
+
/** @enum {string} */
|
|
8881
|
+
classificationState: "unclassified" | "classified";
|
|
8882
|
+
sourceClass: ("board_deck" | "board_minutes" | "investor_update" | "financial_brief" | "budget_forecast" | "okr_plan" | "annual_plan" | "all_hands" | "unit_strategy" | "roadmap" | "qbr" | "other" | "decoy") | null;
|
|
8880
8883
|
periodLabel: string | null;
|
|
8881
8884
|
seriesRole: ("current" | "history") | null;
|
|
8882
8885
|
relevance: number;
|
|
@@ -8886,9 +8889,42 @@ export interface components {
|
|
|
8886
8889
|
sharingBreadth: "restricted_few" | "group" | "domain" | "anyone" | "unknown";
|
|
8887
8890
|
sharedWithCount: number | null;
|
|
8888
8891
|
audienceFit: ("fits" | "widens") | null;
|
|
8889
|
-
|
|
8890
|
-
recommendedUse: "attached" | "direction_only" | "excluded";
|
|
8892
|
+
recommendedUse: ("attached" | "direction_only" | "excluded") | null;
|
|
8891
8893
|
use: ("attached" | "direction_only" | "excluded") | null;
|
|
8894
|
+
selectionSource: ("person" | "approval_default") | null;
|
|
8895
|
+
personHoldReasons: ("relevance_override" | "treatment_selection" | "target_override" | "manual_add")[];
|
|
8896
|
+
/** @enum {string} */
|
|
8897
|
+
machineState: "fresh" | "refreshed" | "filtered_out" | "not_rediscovered" | "pending_classification" | "retired";
|
|
8898
|
+
relevanceReasons: ({
|
|
8899
|
+
/** @constant */
|
|
8900
|
+
kind: "lexicon";
|
|
8901
|
+
/** @enum {string} */
|
|
8902
|
+
code: "title_term" | "folder_term" | "negative_title_term" | "negative_folder_term";
|
|
8903
|
+
term: string;
|
|
8904
|
+
count: number;
|
|
8905
|
+
} | {
|
|
8906
|
+
/** @constant */
|
|
8907
|
+
kind: "signal";
|
|
8908
|
+
/** @enum {string} */
|
|
8909
|
+
code: "folder_probe_hit" | "title_probe_hit" | "version_churn" | "mime_class" | "size_band" | "dated_series" | "recent_edit" | "owner_unit_head" | "owner_executive" | "sharing_breadth" | "duplicate_title" | "personal_mime";
|
|
8910
|
+
value: number | null;
|
|
8911
|
+
} | {
|
|
8912
|
+
/** @constant */
|
|
8913
|
+
kind: "evaluation";
|
|
8914
|
+
/** @enum {string} */
|
|
8915
|
+
code: "not_evaluated_t1" | "not_evaluated_t2" | "folder_unresolved" | "folder_truncated" | "explored_t1" | "explored_t2";
|
|
8916
|
+
} | {
|
|
8917
|
+
/** @constant */
|
|
8918
|
+
kind: "model";
|
|
8919
|
+
/** @enum {string} */
|
|
8920
|
+
code: "title_path_score" | "peek_confirms" | "peek_contradicts";
|
|
8921
|
+
score: number | null;
|
|
8922
|
+
})[];
|
|
8923
|
+
targetAudience: ("restricted" | "unit" | "org_wide") | null;
|
|
8924
|
+
attention: {
|
|
8925
|
+
t1: ("exploit" | "explore") | null;
|
|
8926
|
+
t2: ("exploit" | "explore") | null;
|
|
8927
|
+
};
|
|
8892
8928
|
included: boolean;
|
|
8893
8929
|
treatmentUnresolved: boolean;
|
|
8894
8930
|
matchReason: string;
|
|
@@ -8931,6 +8967,9 @@ export interface components {
|
|
|
8931
8967
|
files: number;
|
|
8932
8968
|
filesIncluded: number;
|
|
8933
8969
|
filesNeedingChoice: number;
|
|
8970
|
+
filesHidden: number;
|
|
8971
|
+
filesNotRediscovered: number;
|
|
8972
|
+
filesPendingClassification: number;
|
|
8934
8973
|
};
|
|
8935
8974
|
scopes: {
|
|
8936
8975
|
scopeKey: string;
|
|
@@ -8953,6 +8992,16 @@ export interface components {
|
|
|
8953
8992
|
highMicroUsd: string;
|
|
8954
8993
|
capMicroUsd: string;
|
|
8955
8994
|
} | null;
|
|
8995
|
+
discovery: {
|
|
8996
|
+
foundThisRun: number;
|
|
8997
|
+
retainedDecisions: number;
|
|
8998
|
+
notRediscovered: number;
|
|
8999
|
+
pendingClassification: number;
|
|
9000
|
+
filesDropped: number;
|
|
9001
|
+
coveragePercent: number;
|
|
9002
|
+
/** @enum {string} */
|
|
9003
|
+
scanOutcome: "complete" | "budget_exhausted" | "file_ceiling" | "stopped";
|
|
9004
|
+
} | null;
|
|
8956
9005
|
};
|
|
8957
9006
|
SourceSyncApproveResponse: {
|
|
8958
9007
|
/** Format: uuid */
|
|
@@ -17884,6 +17933,8 @@ export interface operations {
|
|
|
17884
17933
|
requestBody: {
|
|
17885
17934
|
content: {
|
|
17886
17935
|
"application/json": {
|
|
17936
|
+
/** @constant */
|
|
17937
|
+
visibility?: "shown";
|
|
17887
17938
|
/** @enum {string} */
|
|
17888
17939
|
use?: "attached" | "direction_only" | "excluded";
|
|
17889
17940
|
/** @enum {string} */
|
package/src/decisions/README.md
CHANGED
|
@@ -70,6 +70,29 @@ decision-effect-is-a-domain-parameter.
|
|
|
70
70
|
is a bounded open string because each domain has its own kinds. Promoted
|
|
71
71
|
because the backend renders it and the app consumes it.
|
|
72
72
|
|
|
73
|
+
## Measured evidence is optional
|
|
74
|
+
|
|
75
|
+
`confidence` and `signals` on a `DecisionQuestion` are **present only when a
|
|
76
|
+
derivation measured something**. Structure inference measures a model
|
|
77
|
+
confidence and competing signals, so it supplies both. A deterministic
|
|
78
|
+
derivation — Source Sync's synthesis and treatment questions, where a rule fired
|
|
79
|
+
— measured nothing, so it omits both rather than claiming `confidence: 1` and
|
|
80
|
+
`signals: {}`. An unmeasured confidence is not evidence, and one domain never
|
|
81
|
+
invents data to conform to another's historical type (control ADR
|
|
82
|
+
`source-discovery-relevance-funnel`).
|
|
83
|
+
|
|
84
|
+
- **Readers branch on presence.** Absence means "nothing was measured", which is
|
|
85
|
+
information; a reader that substitutes a number or an empty record erases it.
|
|
86
|
+
Nothing in this package defaults either field — no `?? 1`, no `?? {}` — and a
|
|
87
|
+
test scans `src/decisions/` to keep it that way.
|
|
88
|
+
- **The recommendation is unaffected.** A deterministic question still states
|
|
89
|
+
its belief through `recommendation.strength` (`confident` is a deterministic
|
|
90
|
+
read of recorded facts); `DecisionRecommendation` does not change.
|
|
91
|
+
- **The wire is unchanged.** `DecisionQuestionPresentation` carries its own
|
|
92
|
+
evidence and never carried these fields.
|
|
93
|
+
|
|
94
|
+
See decision-question-evidence-is-optional.
|
|
95
|
+
|
|
73
96
|
## Dependencies
|
|
74
97
|
|
|
75
98
|
`zod` only. This package stays pure vocabulary — no domain logic, no I/O.
|
|
@@ -43,7 +43,16 @@ the payload and its **label** is only prose.
|
|
|
43
43
|
tree and never regenerated. It is the proof that re-expressing a const as a
|
|
44
44
|
factory application left what it accepts unchanged — something neither the
|
|
45
45
|
type checker nor the sampled cases above can see. It is blind to refinements,
|
|
46
|
-
which stay locked by `schemas.test.ts`.
|
|
46
|
+
which stay locked by `schemas.test.ts`. Its one intentional edit is
|
|
47
|
+
decision-question-evidence-is-optional removing `confidence` and `signals`
|
|
48
|
+
from the question schemas' `required` arrays — a contract change, not drift.
|
|
49
|
+
|
|
50
|
+
- `question-evidence-optional.test.ts` — measured evidence is optional
|
|
51
|
+
(decision-question-evidence-is-optional): a deterministic question parses with
|
|
52
|
+
neither `confidence` nor `signals` and none is invented on the way through; a
|
|
53
|
+
measured question keeps both; and a source scan of `src/decisions/*.ts` finds
|
|
54
|
+
no nullish-coalescing default for either field, because readers branch on
|
|
55
|
+
presence.
|
|
47
56
|
|
|
48
57
|
- `generic-effect-fixtures.ts` — the compile-time claims of the effect
|
|
49
58
|
parameter (decision-effect-is-a-domain-parameter). A compiled module, not a
|
|
@@ -22,6 +22,12 @@ import * as schemas from "../schemas";
|
|
|
22
22
|
* fails, the change altered what a schema accepts: fix the code, do not update
|
|
23
23
|
* the golden to make it green.
|
|
24
24
|
*
|
|
25
|
+
* One deliberate exception, a change to the contract rather than a refactor:
|
|
26
|
+
* decision-question-evidence-is-optional made a question's `confidence` and
|
|
27
|
+
* `signals` optional, and exactly those two entries were removed from the
|
|
28
|
+
* `required` arrays of `DecisionQuestionBaseSchema` and `DecisionQuestionSchema`.
|
|
29
|
+
* Their property definitions are untouched.
|
|
30
|
+
*
|
|
25
31
|
* What this cannot see: refinements. `toJSONSchema` drops `superRefine`, so
|
|
26
32
|
* `DecisionQuestionSchema` renders like its base; the cross-field rules stay
|
|
27
33
|
* locked by `schemas.test.ts`.
|
|
@@ -854,9 +854,7 @@
|
|
|
854
854
|
"defaultOptionId",
|
|
855
855
|
"recommendation",
|
|
856
856
|
"multiSelect",
|
|
857
|
-
"freeFormAllowed"
|
|
858
|
-
"confidence",
|
|
859
|
-
"signals"
|
|
857
|
+
"freeFormAllowed"
|
|
860
858
|
],
|
|
861
859
|
"additionalProperties": false
|
|
862
860
|
},
|
|
@@ -1100,9 +1098,7 @@
|
|
|
1100
1098
|
"defaultOptionId",
|
|
1101
1099
|
"recommendation",
|
|
1102
1100
|
"multiSelect",
|
|
1103
|
-
"freeFormAllowed"
|
|
1104
|
-
"confidence",
|
|
1105
|
-
"signals"
|
|
1101
|
+
"freeFormAllowed"
|
|
1106
1102
|
],
|
|
1107
1103
|
"additionalProperties": false
|
|
1108
1104
|
},
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A question's measured evidence is optional (decision-question-evidence-is-optional).
|
|
3
|
+
*
|
|
4
|
+
* Structure inference measures a model confidence and competing signals; a
|
|
5
|
+
* deterministic derivation — a rule that fired — measures nothing. Requiring
|
|
6
|
+
* both fields forced the deterministic domain to invent `confidence: 1` and an
|
|
7
|
+
* empty signals record to share the type, and an unmeasured confidence is not
|
|
8
|
+
* evidence. These lock the three halves: a deterministic question is valid
|
|
9
|
+
* without either field and gains neither on the way through the schema, a
|
|
10
|
+
* measured question loses neither, and nothing in this domain defaults them —
|
|
11
|
+
* readers branch on presence.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { describe, expect, it } from "vitest";
|
|
18
|
+
import {
|
|
19
|
+
DecisionQuestionSchema,
|
|
20
|
+
type DecisionOption,
|
|
21
|
+
type DecisionQuestion,
|
|
22
|
+
} from "../index";
|
|
23
|
+
|
|
24
|
+
const KEEP: DecisionOption = {
|
|
25
|
+
id: "keep",
|
|
26
|
+
label: "Keep the unit where it is.",
|
|
27
|
+
effect: { kind: "none" },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ARCHIVE: DecisionOption = {
|
|
31
|
+
id: "archive",
|
|
32
|
+
label:
|
|
33
|
+
"Archive the unit; no one has been assigned to it since it was created.",
|
|
34
|
+
effect: {
|
|
35
|
+
kind: "archive_unit",
|
|
36
|
+
unitId: "5b0f6a36-6c1e-4f59-9a55-0c4b3d0f2a11",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** A rule fired: the recommendation is a read of recorded facts, not a measurement. */
|
|
41
|
+
const DETERMINISTIC_QUESTION: DecisionQuestion = {
|
|
42
|
+
id: "empty_unit:5b0f6a36",
|
|
43
|
+
header: "Empty unit",
|
|
44
|
+
question: "Should the Facilities unit, which has no members, be archived?",
|
|
45
|
+
options: [KEEP, ARCHIVE],
|
|
46
|
+
defaultOptionId: "keep",
|
|
47
|
+
recommendation: { optionId: "archive", strength: "confident" },
|
|
48
|
+
multiSelect: false,
|
|
49
|
+
freeFormAllowed: false,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const MEASURED_QUESTION: DecisionQuestion = {
|
|
53
|
+
...DETERMINISTIC_QUESTION,
|
|
54
|
+
id: "empty_unit:measured",
|
|
55
|
+
recommendation: { optionId: "archive", strength: "tentative" },
|
|
56
|
+
confidence: 0.62,
|
|
57
|
+
signals: {
|
|
58
|
+
Membership: "No person has listed Facilities as their department.",
|
|
59
|
+
Documents: "Three documents still name Facilities as their owner.",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe("a DecisionQuestion's measured evidence", () => {
|
|
64
|
+
it("a deterministic question parses with neither confidence nor signals", () => {
|
|
65
|
+
const parsed = DecisionQuestionSchema.parse(DETERMINISTIC_QUESTION);
|
|
66
|
+
// Absent, not defaulted: the schema must not invent a measurement.
|
|
67
|
+
expect("confidence" in parsed).toBe(false);
|
|
68
|
+
expect("signals" in parsed).toBe(false);
|
|
69
|
+
expect(parsed.confidence).toBeUndefined();
|
|
70
|
+
expect(parsed.signals).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("a measured question keeps both", () => {
|
|
74
|
+
const parsed = DecisionQuestionSchema.parse(MEASURED_QUESTION);
|
|
75
|
+
expect(parsed.confidence).toBe(0.62);
|
|
76
|
+
expect(parsed.signals).toEqual(MEASURED_QUESTION.signals);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("still bounds a confidence that is present", () => {
|
|
80
|
+
expect(
|
|
81
|
+
DecisionQuestionSchema.safeParse({
|
|
82
|
+
...MEASURED_QUESTION,
|
|
83
|
+
confidence: 1.5,
|
|
84
|
+
}).success,
|
|
85
|
+
).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("nothing in decisions defaults the evidence", () => {
|
|
89
|
+
const dir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
90
|
+
const sources = readdirSync(dir).filter((file) => file.endsWith(".ts"));
|
|
91
|
+
expect(sources.length).toBeGreaterThan(0);
|
|
92
|
+
const defaulting = /confidence \?\?|signals \?\?/;
|
|
93
|
+
for (const file of sources) {
|
|
94
|
+
const found = readFileSync(join(dir, file), "utf8").match(defaulting);
|
|
95
|
+
expect(
|
|
96
|
+
found?.[0],
|
|
97
|
+
`${file} defaults a question's measured evidence; branch on presence instead`,
|
|
98
|
+
).toBeUndefined();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
package/src/decisions/schemas.ts
CHANGED
|
@@ -94,8 +94,8 @@ export function decisionQuestionBaseSchemaOf<TEffect extends EffectSchema>(
|
|
|
94
94
|
recommendation: DecisionRecommendationSchema.nullable(),
|
|
95
95
|
multiSelect: z.boolean(),
|
|
96
96
|
freeFormAllowed: z.boolean(),
|
|
97
|
-
confidence: z.number().min(0).max(1),
|
|
98
|
-
signals: z.record(z.string(), z.string()),
|
|
97
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
98
|
+
signals: z.record(z.string(), z.string()).optional(),
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
|
package/src/decisions/types.ts
CHANGED
|
@@ -241,13 +241,21 @@ export interface DecisionQuestion<
|
|
|
241
241
|
*/
|
|
242
242
|
readonly freeFormAllowed: boolean;
|
|
243
243
|
/**
|
|
244
|
-
* How sure the derivation is of its finding, 0..1
|
|
245
|
-
*
|
|
244
|
+
* How sure the derivation is of its finding, 0..1 — PRESENT only when something
|
|
245
|
+
* was measured. A deterministic derivation (a rule that fired) omits it rather
|
|
246
|
+
* than claiming 1: an unmeasured confidence is not evidence (contracts ADR
|
|
247
|
+
* decision-question-evidence-is-optional). Readers branch on presence; nothing
|
|
248
|
+
* defaults it. The human-facing reading is
|
|
249
|
+
* {@link DecisionRecommendation.strength}; this number is for ranking and
|
|
246
250
|
* calibration, not for display.
|
|
247
251
|
*/
|
|
248
|
-
readonly confidence
|
|
249
|
-
/**
|
|
250
|
-
|
|
252
|
+
readonly confidence?: number;
|
|
253
|
+
/**
|
|
254
|
+
* Each competing signal and a one-line reading of what it says. PRESENT only
|
|
255
|
+
* when signals were measured; readers branch on presence, and nothing defaults
|
|
256
|
+
* it to an empty record.
|
|
257
|
+
*/
|
|
258
|
+
readonly signals?: Readonly<Record<string, string>>;
|
|
251
259
|
}
|
|
252
260
|
|
|
253
261
|
/** One answer to one question. */
|
|
@@ -107,6 +107,54 @@ way. This directory holds that shared spelling.
|
|
|
107
107
|
domain is the largest trust change in the wave, and an empty body is
|
|
108
108
|
indistinguishable from a mistaken or replayed body-less POST.
|
|
109
109
|
|
|
110
|
+
## Relevance
|
|
111
|
+
|
|
112
|
+
Doctrine: control ADR `source-discovery-relevance-funnel`. Vocabulary: ADR
|
|
113
|
+
`source-discovery-relevance-vocabulary`. The sets live in `relevance.ts`.
|
|
114
|
+
|
|
115
|
+
- **Two axes on the wire.**
|
|
116
|
+
- **Relevance** is the funnel's verdict: `visibility` is `shown` or
|
|
117
|
+
`hidden`.
|
|
118
|
+
- **Treatment** is a person's decision: `recommendedUse` (machine state,
|
|
119
|
+
null until classified), `use` (the committed treatment, null until
|
|
120
|
+
chosen) and `selectionSource` (`person` or `approval_default`, present
|
|
121
|
+
exactly when `use` is).
|
|
122
|
+
|
|
123
|
+
Neither axis changes the other. The disclosure question is asked only of
|
|
124
|
+
lines that cleared relevance.
|
|
125
|
+
|
|
126
|
+
- **Hidden is not dropped.** `SOURCE_RELEVANCE_TIERS` includes `dropped` so the
|
|
127
|
+
backend can mirror it in persistence, but the item schema excludes it: a
|
|
128
|
+
dropped sighting never reaches the wire. A hidden line sits behind the
|
|
129
|
+
drawer, is never `included`, and a person can bring it into review with the
|
|
130
|
+
patch `{ visibility: "shown" }`. The patch changes exactly one field, so
|
|
131
|
+
promoting a line never also commits a treatment, and no client can hide a
|
|
132
|
+
line.
|
|
133
|
+
- **Classification is separate from visibility.** `classificationState` is
|
|
134
|
+
`unclassified` (so `sourceClass` and `recommendedUse` are both null) or
|
|
135
|
+
`classified` (both non-null). A shown line can still be
|
|
136
|
+
`pending_classification` in its `machineState`.
|
|
137
|
+
- **Reasons are a discriminated union.** `relevanceReasons` (≤ 8) is
|
|
138
|
+
`RelevanceReason`: `lexicon` (a product-owned `term` and a `count`),
|
|
139
|
+
`signal` (a `value` or null), `evaluation` (a code only) or `model` (a
|
|
140
|
+
`score` or null). Each kind has its own disjoint code set, and the app
|
|
141
|
+
switches over `kind`, then `code`, exhaustively. `relevance` is the fused
|
|
142
|
+
score, and `matchReason` is server copy rendered from the reasons
|
|
143
|
+
(deprecated).
|
|
144
|
+
- **Holds and lifecycle travel on the item.** `personHoldReasons` are the
|
|
145
|
+
field-level barriers a person's actions placed on the line. `machineState`
|
|
146
|
+
is its lifecycle (`fresh`, `refreshed`, `filtered_out`, `not_rediscovered`,
|
|
147
|
+
`pending_classification`, `retired`). `attention` says which lane
|
|
148
|
+
(`exploit` or `explore`) earned each model tier's look.
|
|
149
|
+
- **Attach shows its audience.** `targetAudience` is the audience Attach would
|
|
150
|
+
disclose to. Null means unknown and reads as "may widen".
|
|
151
|
+
- **No single ambiguous total.** `discovery` on the set response carries the
|
|
152
|
+
last completed run's counts (found, retained decisions, not rediscovered,
|
|
153
|
+
pending classification, dropped, coverage and how the scan ended). It is
|
|
154
|
+
null before any run completes. `totals` adds `filesHidden`,
|
|
155
|
+
`filesNotRediscovered` and `filesPendingClassification`, counted by
|
|
156
|
+
`candidateTotalsOf` over distinct files.
|
|
157
|
+
|
|
110
158
|
## Public API
|
|
111
159
|
|
|
112
160
|
- `SOURCE_CLASSES` / `SourceClass` — what kind of document a source is.
|
|
@@ -135,7 +183,16 @@ way. This directory holds that shared spelling.
|
|
|
135
183
|
`candidateTotalsOf(items)` — THE file count: distinct `sourceId`, never lines
|
|
136
184
|
(ADR-CONTRACTS-195).
|
|
137
185
|
- `SourceSyncCandidateItemPatchSchema` / `SourceSyncCandidateItemPatch` — one
|
|
138
|
-
edit to one candidate item
|
|
186
|
+
edit to one candidate item: exactly one of visibility (`shown` only), use,
|
|
187
|
+
class or target.
|
|
188
|
+
- `SOURCE_RELEVANCE_TIERS`, `CLASSIFICATION_STATES`, `RELEVANCE_REASON_KINDS`,
|
|
189
|
+
`LEXICON_REASON_CODES`, `SIGNAL_REASON_CODES`, `EVALUATION_REASON_CODES`,
|
|
190
|
+
`MODEL_REASON_CODES`, `TARGET_AUDIENCES`, `ATTENTION_LANES`,
|
|
191
|
+
`CANDIDATE_MACHINE_STATES`, `SELECTION_SOURCES`, `PERSON_HOLD_REASONS`,
|
|
192
|
+
`SCAN_OUTCOMES` and their derived unions — the relevance vocabulary (see
|
|
193
|
+
Relevance).
|
|
194
|
+
- `RelevanceReasonSchema` / `RelevanceReason` — why a candidate scored as it
|
|
195
|
+
did: a discriminated union on `kind`.
|
|
139
196
|
- `SourceSyncAddCandidateRequestSchema` / `SourceSyncAddCandidateRequest` —
|
|
140
197
|
"add one I missed": a provider file id picked in the provider's own search.
|
|
141
198
|
- `SourceSyncApproveRequestSchema` / `SourceSyncApproveRequest` — approve the
|
|
@@ -210,4 +267,5 @@ way. This directory holds that shared spelling.
|
|
|
210
267
|
|
|
211
268
|
`rules.ts` imports only `zod`, `../api/primitives` and `./vocabulary`.
|
|
212
269
|
`upkeep.ts` and `reach.ts` import only `zod` and `../api/primitives`.
|
|
270
|
+
`relevance.ts` imports only `zod`.
|
|
213
271
|
`vocabulary.ts` itself has no imports.
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* backend answers with.
|
|
6
6
|
*
|
|
7
7
|
* INVARIANTS TESTED:
|
|
8
|
-
* - A candidate patch changes
|
|
9
|
-
* unknown key is an error, not surplus.
|
|
8
|
+
* - A candidate patch changes exactly one thing: an empty patch is refused, and
|
|
9
|
+
* an unknown key is an error, not surplus.
|
|
10
10
|
* - An approval echoes the revision the person reviewed and carries the spend
|
|
11
11
|
* cap as integer micro-USD; either one missing is refused.
|
|
12
12
|
* - An approval answers PER SCOPE: one row per scope approved, each bound to its
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* below, which fails typecheck if one is missing at the root).
|
|
17
17
|
* - The same two checks hold for the Release D names: staleness findings,
|
|
18
18
|
* standing orders and the Drive-reach shapes.
|
|
19
|
+
* - Every runtime name `relevance.ts` declares is reachable from the root as
|
|
20
|
+
* the same binding.
|
|
19
21
|
* - `ACTION_ITEM_KINDS` at the root carries `sourceSync.approval_pending`.
|
|
20
22
|
* - The root re-exports the domain barrel explicitly.
|
|
21
23
|
*
|
|
@@ -66,6 +68,7 @@ import type {
|
|
|
66
68
|
SourceSyncStandingOrdersResponse,
|
|
67
69
|
} from "../../index";
|
|
68
70
|
import * as reach from "../reach";
|
|
71
|
+
import * as relevance from "../relevance";
|
|
69
72
|
import * as rules from "../rules";
|
|
70
73
|
import * as schemas from "../schemas";
|
|
71
74
|
import * as upkeep from "../upkeep";
|
|
@@ -236,6 +239,17 @@ describe("source-sync Release C package surface", () => {
|
|
|
236
239
|
}
|
|
237
240
|
});
|
|
238
241
|
|
|
242
|
+
it("every discovery relevance set is exported from the package root", () => {
|
|
243
|
+
const root = rootBarrel as Record<string, unknown>;
|
|
244
|
+
const names = Object.keys(relevance);
|
|
245
|
+
expect(names.length).toBeGreaterThan(0);
|
|
246
|
+
for (const name of names) {
|
|
247
|
+
expect(root[name], `${name} at the root`).toBe(
|
|
248
|
+
(relevance as Record<string, unknown>)[name],
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
239
253
|
it("exposes sourceSync.approval_pending in ACTION_ITEM_KINDS at the root", () => {
|
|
240
254
|
expect(rootBarrel.ACTION_ITEM_KINDS).toContain(
|
|
241
255
|
"sourceSync.approval_pending",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one file count for a candidate review.
|
|
3
|
+
*
|
|
4
|
+
* INVARIANTS TESTED:
|
|
5
|
+
* - Every total counts DISTINCT files (`sourceId`), never lines: a file routed to
|
|
6
|
+
* two documents is two lines and one file in every total.
|
|
7
|
+
* - A file is hidden only when EVERY line of it is hidden; one shown line shows it.
|
|
8
|
+
* - A file is not rediscovered / pending classification when ANY line of it is.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
import { candidateTotalsOf, type CandidateTotalsInput } from "../index";
|
|
13
|
+
|
|
14
|
+
function line(
|
|
15
|
+
sourceId: string,
|
|
16
|
+
overrides: Partial<CandidateTotalsInput> = {},
|
|
17
|
+
): CandidateTotalsInput {
|
|
18
|
+
return {
|
|
19
|
+
sourceId,
|
|
20
|
+
included: true,
|
|
21
|
+
treatmentUnresolved: false,
|
|
22
|
+
visibility: "shown",
|
|
23
|
+
machineState: "fresh",
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("candidateTotalsOf", () => {
|
|
29
|
+
it("counts distinct files, never lines", () => {
|
|
30
|
+
expect(
|
|
31
|
+
candidateTotalsOf([
|
|
32
|
+
line("a", { treatmentUnresolved: true }),
|
|
33
|
+
line("a"),
|
|
34
|
+
line("b", { included: false, treatmentUnresolved: true }),
|
|
35
|
+
line("c"),
|
|
36
|
+
]),
|
|
37
|
+
).toEqual({
|
|
38
|
+
files: 3,
|
|
39
|
+
filesIncluded: 2,
|
|
40
|
+
filesNeedingChoice: 1,
|
|
41
|
+
filesHidden: 0,
|
|
42
|
+
filesNotRediscovered: 0,
|
|
43
|
+
filesPendingClassification: 0,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("a file is hidden only when every line of it is hidden", () => {
|
|
48
|
+
const hidden = { visibility: "hidden", included: false } as const;
|
|
49
|
+
const totals = candidateTotalsOf([
|
|
50
|
+
line("partly", hidden),
|
|
51
|
+
line("partly"),
|
|
52
|
+
line("wholly", hidden),
|
|
53
|
+
line("wholly", hidden),
|
|
54
|
+
line("shown"),
|
|
55
|
+
]);
|
|
56
|
+
expect(totals.files).toBe(3);
|
|
57
|
+
expect(totals.filesHidden).toBe(1);
|
|
58
|
+
expect(totals.filesIncluded).toBe(2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("a file is not rediscovered or pending classification when any line of it is", () => {
|
|
62
|
+
const totals = candidateTotalsOf([
|
|
63
|
+
line("a", { machineState: "not_rediscovered" }),
|
|
64
|
+
line("a", { machineState: "refreshed" }),
|
|
65
|
+
line("b", { machineState: "pending_classification" }),
|
|
66
|
+
line("b"),
|
|
67
|
+
line("c", { machineState: "retired" }),
|
|
68
|
+
]);
|
|
69
|
+
expect(totals.filesNotRediscovered).toBe(1);
|
|
70
|
+
expect(totals.filesPendingClassification).toBe(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("a file routed to two documents counts once in every total", () => {
|
|
74
|
+
const facts = {
|
|
75
|
+
treatmentUnresolved: true,
|
|
76
|
+
visibility: "hidden",
|
|
77
|
+
machineState: "not_rediscovered",
|
|
78
|
+
included: false,
|
|
79
|
+
} as const;
|
|
80
|
+
const hiddenTwice = candidateTotalsOf([line("a", facts), line("a", facts)]);
|
|
81
|
+
expect(hiddenTwice).toEqual({
|
|
82
|
+
files: 1,
|
|
83
|
+
filesIncluded: 0,
|
|
84
|
+
filesNeedingChoice: 0,
|
|
85
|
+
filesHidden: 1,
|
|
86
|
+
filesNotRediscovered: 1,
|
|
87
|
+
filesPendingClassification: 0,
|
|
88
|
+
});
|
|
89
|
+
const shownTwice = candidateTotalsOf([
|
|
90
|
+
line("a", {
|
|
91
|
+
treatmentUnresolved: true,
|
|
92
|
+
machineState: "pending_classification",
|
|
93
|
+
}),
|
|
94
|
+
line("a", {
|
|
95
|
+
treatmentUnresolved: true,
|
|
96
|
+
machineState: "pending_classification",
|
|
97
|
+
}),
|
|
98
|
+
]);
|
|
99
|
+
expect(shownTwice).toEqual({
|
|
100
|
+
files: 1,
|
|
101
|
+
filesIncluded: 1,
|
|
102
|
+
filesNeedingChoice: 1,
|
|
103
|
+
filesHidden: 0,
|
|
104
|
+
filesNotRediscovered: 0,
|
|
105
|
+
filesPendingClassification: 1,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("counts nothing for an empty review", () => {
|
|
110
|
+
expect(candidateTotalsOf([])).toEqual({
|
|
111
|
+
files: 0,
|
|
112
|
+
filesIncluded: 0,
|
|
113
|
+
filesNeedingChoice: 0,
|
|
114
|
+
filesHidden: 0,
|
|
115
|
+
filesNotRediscovered: 0,
|
|
116
|
+
filesPendingClassification: 0,
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|