@company-semantics/contracts 84.0.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/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/package.json
CHANGED
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. */
|