@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "84.0.0",
3
+ "version": "85.0.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
+ });
@@ -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
 
@@ -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. The human-facing reading of
245
- * it is {@link DecisionRecommendation.strength}; this number is for ranking and
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: number;
249
- /** Each competing signal and a one-line reading of what it says. */
250
- readonly signals: Readonly<Record<string, string>>;
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. */