@company-semantics/contracts 88.1.0 → 89.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 +365 -2
- package/src/decisions/README.md +5 -0
- package/src/decisions/__tests__/presentation.test.ts +153 -0
- package/src/decisions/index.ts +6 -1
- package/src/decisions/presentation.ts +82 -1
- package/src/generated/openapi-routes.ts +4 -0
- package/src/source-sync/README.md +44 -2
- package/src/source-sync/__tests__/barrel.test.ts +11 -1
- package/src/source-sync/__tests__/schemas.test.ts +141 -2
- package/src/source-sync/__tests__/scope-projection.test.ts +1 -1
- package/src/source-sync/__tests__/vocabulary.test.ts +73 -1
- package/src/source-sync/candidate-item.ts +155 -0
- package/src/source-sync/candidate-totals.ts +2 -4
- package/src/source-sync/index.ts +13 -4
- package/src/source-sync/relevance.ts +38 -3
- package/src/source-sync/schemas.ts +5 -127
- package/src/source-sync/vocabulary.ts +45 -0
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 = '280a694d540b' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '280a694d540b48ca0e4f5985e8529cc5a88f96efca70980012b011cc914cc08c' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -3834,6 +3834,80 @@ export interface paths {
|
|
|
3834
3834
|
patch?: never;
|
|
3835
3835
|
trace?: never;
|
|
3836
3836
|
};
|
|
3837
|
+
"/api/factory/quality/changes": {
|
|
3838
|
+
parameters: {
|
|
3839
|
+
query?: never;
|
|
3840
|
+
header?: never;
|
|
3841
|
+
path?: never;
|
|
3842
|
+
cookie?: never;
|
|
3843
|
+
};
|
|
3844
|
+
get?: never;
|
|
3845
|
+
put?: never;
|
|
3846
|
+
/**
|
|
3847
|
+
* Report a pull request head and get the evaluation plan to run for it
|
|
3848
|
+
* @description Resolves the changed files to an impact set, records the change and plans its evaluation. Idempotent: the same head reported again answers the same plan. A change no impact rule matches answers { noImpact: true } and records nothing.
|
|
3849
|
+
*/
|
|
3850
|
+
post: operations["createQualityChange"];
|
|
3851
|
+
delete?: never;
|
|
3852
|
+
options?: never;
|
|
3853
|
+
head?: never;
|
|
3854
|
+
patch?: never;
|
|
3855
|
+
trace?: never;
|
|
3856
|
+
};
|
|
3857
|
+
"/api/factory/quality/plans/{planId}": {
|
|
3858
|
+
parameters: {
|
|
3859
|
+
query?: never;
|
|
3860
|
+
header?: never;
|
|
3861
|
+
path?: never;
|
|
3862
|
+
cookie?: never;
|
|
3863
|
+
};
|
|
3864
|
+
/** Read an evaluation plan: suites, pinned manifests, samples, model profile, budget */
|
|
3865
|
+
get: operations["getQualityPlan"];
|
|
3866
|
+
put?: never;
|
|
3867
|
+
post?: never;
|
|
3868
|
+
delete?: never;
|
|
3869
|
+
options?: never;
|
|
3870
|
+
head?: never;
|
|
3871
|
+
patch?: never;
|
|
3872
|
+
trace?: never;
|
|
3873
|
+
};
|
|
3874
|
+
"/api/factory/quality/plans/{planId}/decision": {
|
|
3875
|
+
parameters: {
|
|
3876
|
+
query?: never;
|
|
3877
|
+
header?: never;
|
|
3878
|
+
path?: never;
|
|
3879
|
+
cookie?: never;
|
|
3880
|
+
};
|
|
3881
|
+
/** Read where a plan's evaluation stands and, once recorded, its decision */
|
|
3882
|
+
get: operations["getQualityPlanDecision"];
|
|
3883
|
+
put?: never;
|
|
3884
|
+
post?: never;
|
|
3885
|
+
delete?: never;
|
|
3886
|
+
options?: never;
|
|
3887
|
+
head?: never;
|
|
3888
|
+
patch?: never;
|
|
3889
|
+
trace?: never;
|
|
3890
|
+
};
|
|
3891
|
+
"/api/factory/quality/deployments": {
|
|
3892
|
+
parameters: {
|
|
3893
|
+
query?: never;
|
|
3894
|
+
header?: never;
|
|
3895
|
+
path?: never;
|
|
3896
|
+
cookie?: never;
|
|
3897
|
+
};
|
|
3898
|
+
get?: never;
|
|
3899
|
+
put?: never;
|
|
3900
|
+
/**
|
|
3901
|
+
* Report one phase of a deploy run so an applied backend deployment is verified
|
|
3902
|
+
* @description An applied phase that names a backend commit pins the release references, records the deployment as a change to verify, plans the verification and asks for its runs. Idempotent: the same deployment reported again answers the same change and plan. Any other report answers 202 { ignored: true } and records nothing. The deployment is never held for its verification.
|
|
3903
|
+
*/
|
|
3904
|
+
post: operations["recordQualityDeployment"];
|
|
3905
|
+
delete?: never;
|
|
3906
|
+
options?: never;
|
|
3907
|
+
head?: never;
|
|
3908
|
+
patch?: never;
|
|
3909
|
+
trace?: never;
|
|
3910
|
+
};
|
|
3837
3911
|
"/api/internal-admin/impersonate/start": {
|
|
3838
3912
|
parameters: {
|
|
3839
3913
|
query?: never;
|
|
@@ -8255,6 +8329,87 @@ export interface components {
|
|
|
8255
8329
|
[key: string]: unknown;
|
|
8256
8330
|
} | null;
|
|
8257
8331
|
};
|
|
8332
|
+
CreateQualityChangeResponse: {
|
|
8333
|
+
/** Format: uuid */
|
|
8334
|
+
planId: string;
|
|
8335
|
+
suites: string[];
|
|
8336
|
+
samples: number;
|
|
8337
|
+
budgetUsd: number;
|
|
8338
|
+
modelProfile: {
|
|
8339
|
+
[key: string]: unknown;
|
|
8340
|
+
};
|
|
8341
|
+
} | {
|
|
8342
|
+
/** @constant */
|
|
8343
|
+
noImpact: true;
|
|
8344
|
+
};
|
|
8345
|
+
CreateQualityChangeRequest: {
|
|
8346
|
+
/** @constant */
|
|
8347
|
+
sourceKind: "pull_request";
|
|
8348
|
+
sourceRef: string;
|
|
8349
|
+
gitSha: string;
|
|
8350
|
+
changedFiles: string[];
|
|
8351
|
+
};
|
|
8352
|
+
QualityPlanResponse: {
|
|
8353
|
+
/** Format: uuid */
|
|
8354
|
+
planId: string;
|
|
8355
|
+
suites: string[];
|
|
8356
|
+
samples: number;
|
|
8357
|
+
budgetUsd: number;
|
|
8358
|
+
modelProfile: {
|
|
8359
|
+
[key: string]: unknown;
|
|
8360
|
+
};
|
|
8361
|
+
/** Format: uuid */
|
|
8362
|
+
changeId: string;
|
|
8363
|
+
/** @enum {string} */
|
|
8364
|
+
state: "active" | "superseded";
|
|
8365
|
+
supersededBy: string | null;
|
|
8366
|
+
manifests: {
|
|
8367
|
+
suite: string;
|
|
8368
|
+
manifestId: string;
|
|
8369
|
+
entries: {
|
|
8370
|
+
caseId: string;
|
|
8371
|
+
caseRevision: string;
|
|
8372
|
+
}[] | null;
|
|
8373
|
+
}[];
|
|
8374
|
+
};
|
|
8375
|
+
QualityPlanDecisionResponse: {
|
|
8376
|
+
/** @enum {string} */
|
|
8377
|
+
state: "queued" | "running" | "blocked" | "complete";
|
|
8378
|
+
/** @enum {string} */
|
|
8379
|
+
blockReason?: "budget_exhausted" | "grader_calibration_pending" | "required_run_pending" | "provider_unavailable" | "manifest_unavailable";
|
|
8380
|
+
decision?: {
|
|
8381
|
+
/** @enum {string} */
|
|
8382
|
+
outcome: "promote" | "reject" | "escalate";
|
|
8383
|
+
reasons: {
|
|
8384
|
+
/** @enum {string} */
|
|
8385
|
+
code: "critical_regression" | "invariant_regression" | "protected_case_failed" | "below_floor" | "bucket_budget_exceeded" | "insufficient_improvement" | "cost_regression" | "latency_regression" | "grader_ineligible" | "insufficient_samples" | "flaky_case" | "policy_ambiguous" | "all_gates_passed" | "human_override";
|
|
8386
|
+
detail?: string;
|
|
8387
|
+
}[];
|
|
8388
|
+
};
|
|
8389
|
+
/** Format: uuid */
|
|
8390
|
+
supersededBy?: string;
|
|
8391
|
+
};
|
|
8392
|
+
RecordQualityDeploymentResponse: {
|
|
8393
|
+
/** Format: uuid */
|
|
8394
|
+
changeId: string;
|
|
8395
|
+
/** Format: uuid */
|
|
8396
|
+
planId: string;
|
|
8397
|
+
releaseReferences: number;
|
|
8398
|
+
};
|
|
8399
|
+
QualityDeploymentIgnored: {
|
|
8400
|
+
/** @constant */
|
|
8401
|
+
ignored: true;
|
|
8402
|
+
};
|
|
8403
|
+
RecordQualityDeploymentRequest: {
|
|
8404
|
+
manifestId: string | null;
|
|
8405
|
+
phase: string;
|
|
8406
|
+
shas: {
|
|
8407
|
+
backend?: string;
|
|
8408
|
+
app?: string;
|
|
8409
|
+
edge?: string;
|
|
8410
|
+
} | null;
|
|
8411
|
+
surfaces: string[];
|
|
8412
|
+
};
|
|
8258
8413
|
ImpersonationSessionResponse: {
|
|
8259
8414
|
session: {
|
|
8260
8415
|
impersonationSessionId: string;
|
|
@@ -8816,6 +8971,8 @@ export interface components {
|
|
|
8816
8971
|
/** @enum {string} */
|
|
8817
8972
|
classificationState: "unclassified" | "classified";
|
|
8818
8973
|
sourceClass: ("board_deck" | "board_minutes" | "investor_update" | "financial_brief" | "budget_forecast" | "okr_plan" | "annual_plan" | "all_hands" | "unit_strategy" | "roadmap" | "qbr" | "other" | "decoy") | null;
|
|
8974
|
+
documentType: ("board_deck" | "board_minutes" | "strategy_memo" | "planning_doc" | "okr_doc" | "roadmap" | "operating_review" | "all_hands_deck" | "offsite_notes" | "investor_update" | "pitch_deck" | "product_spec" | "research_analysis" | "general_meeting_notes" | "financial_plan" | "financial_actuals" | "equity_admin_record" | "corporate_formation_record" | "corporate_governance_record" | "government_filing" | "equity_agreement" | "commercial_agreement" | "employment_agreement" | "transaction_record" | "personal_document" | "social_event_material" | "coursework" | "empty_template" | "image_or_screenshot" | "unknown") | null;
|
|
8975
|
+
documentTypeConfidence: number | null;
|
|
8819
8976
|
periodLabel: string | null;
|
|
8820
8977
|
seriesRole: ("current" | "history") | null;
|
|
8821
8978
|
relevance: number;
|
|
@@ -8848,12 +9005,12 @@ export interface components {
|
|
|
8848
9005
|
/** @constant */
|
|
8849
9006
|
kind: "evaluation";
|
|
8850
9007
|
/** @enum {string} */
|
|
8851
|
-
code: "not_evaluated_t1" | "not_evaluated_t2" | "folder_unresolved" | "folder_truncated" | "explored_t1" | "explored_t2";
|
|
9008
|
+
code: "not_evaluated_t1" | "not_evaluated_t2" | "folder_unresolved" | "folder_truncated" | "explored_t1" | "explored_t2" | "peek_insufficient_evidence" | "type_disagreement" | "type_below_confidence" | "execution_failed_t1" | "execution_failed_t2";
|
|
8852
9009
|
} | {
|
|
8853
9010
|
/** @constant */
|
|
8854
9011
|
kind: "model";
|
|
8855
9012
|
/** @enum {string} */
|
|
8856
|
-
code: "title_path_score" | "peek_confirms" | "peek_contradicts";
|
|
9013
|
+
code: "title_path_score" | "peek_confirms" | "peek_contradicts" | "type_policy" | "peek_strategy_evidence_present" | "peek_strategy_evidence_absent";
|
|
8857
9014
|
score: number | null;
|
|
8858
9015
|
})[];
|
|
8859
9016
|
targetAudience: ("restricted" | "unit" | "org_wide") | null;
|
|
@@ -8897,6 +9054,20 @@ export interface components {
|
|
|
8897
9054
|
text: string;
|
|
8898
9055
|
detail?: string;
|
|
8899
9056
|
};
|
|
9057
|
+
rows?: {
|
|
9058
|
+
rowId: string;
|
|
9059
|
+
label: string;
|
|
9060
|
+
note?: string;
|
|
9061
|
+
evidence: {
|
|
9062
|
+
label: string;
|
|
9063
|
+
value: string;
|
|
9064
|
+
withheld: boolean;
|
|
9065
|
+
}[];
|
|
9066
|
+
selectedChoiceId: string | null;
|
|
9067
|
+
recommendedChoiceId: string | null;
|
|
9068
|
+
allowedChoiceIds: string[];
|
|
9069
|
+
required: boolean;
|
|
9070
|
+
}[];
|
|
8900
9071
|
}[];
|
|
8901
9072
|
withheldQuestionCount: number;
|
|
8902
9073
|
totals: {
|
|
@@ -9023,6 +9194,20 @@ export interface components {
|
|
|
9023
9194
|
text: string;
|
|
9024
9195
|
detail?: string;
|
|
9025
9196
|
};
|
|
9197
|
+
rows?: {
|
|
9198
|
+
rowId: string;
|
|
9199
|
+
label: string;
|
|
9200
|
+
note?: string;
|
|
9201
|
+
evidence: {
|
|
9202
|
+
label: string;
|
|
9203
|
+
value: string;
|
|
9204
|
+
withheld: boolean;
|
|
9205
|
+
}[];
|
|
9206
|
+
selectedChoiceId: string | null;
|
|
9207
|
+
recommendedChoiceId: string | null;
|
|
9208
|
+
allowedChoiceIds: string[];
|
|
9209
|
+
required: boolean;
|
|
9210
|
+
}[];
|
|
9026
9211
|
}[];
|
|
9027
9212
|
withheldQuestionCount: number;
|
|
9028
9213
|
changes: {
|
|
@@ -16423,6 +16608,184 @@ export interface operations {
|
|
|
16423
16608
|
};
|
|
16424
16609
|
};
|
|
16425
16610
|
};
|
|
16611
|
+
createQualityChange: {
|
|
16612
|
+
parameters: {
|
|
16613
|
+
query?: never;
|
|
16614
|
+
header?: never;
|
|
16615
|
+
path?: never;
|
|
16616
|
+
cookie?: never;
|
|
16617
|
+
};
|
|
16618
|
+
requestBody: {
|
|
16619
|
+
content: {
|
|
16620
|
+
"application/json": components["schemas"]["CreateQualityChangeRequest"];
|
|
16621
|
+
};
|
|
16622
|
+
};
|
|
16623
|
+
responses: {
|
|
16624
|
+
/** @description The plan to run (planId, suites, samples, budgetUsd, modelProfile), or noImpact */
|
|
16625
|
+
200: {
|
|
16626
|
+
headers: {
|
|
16627
|
+
[name: string]: unknown;
|
|
16628
|
+
};
|
|
16629
|
+
content: {
|
|
16630
|
+
"application/json": components["schemas"]["CreateQualityChangeResponse"];
|
|
16631
|
+
};
|
|
16632
|
+
};
|
|
16633
|
+
/** @description Payload invalid */
|
|
16634
|
+
400: {
|
|
16635
|
+
headers: {
|
|
16636
|
+
[name: string]: unknown;
|
|
16637
|
+
};
|
|
16638
|
+
content?: never;
|
|
16639
|
+
};
|
|
16640
|
+
/** @description Missing or invalid Floor bearer token */
|
|
16641
|
+
401: {
|
|
16642
|
+
headers: {
|
|
16643
|
+
[name: string]: unknown;
|
|
16644
|
+
};
|
|
16645
|
+
content?: never;
|
|
16646
|
+
};
|
|
16647
|
+
/** @description The change was replaced; its replacement is what is planned */
|
|
16648
|
+
409: {
|
|
16649
|
+
headers: {
|
|
16650
|
+
[name: string]: unknown;
|
|
16651
|
+
};
|
|
16652
|
+
content?: never;
|
|
16653
|
+
};
|
|
16654
|
+
};
|
|
16655
|
+
};
|
|
16656
|
+
getQualityPlan: {
|
|
16657
|
+
parameters: {
|
|
16658
|
+
query?: never;
|
|
16659
|
+
header?: never;
|
|
16660
|
+
path: {
|
|
16661
|
+
planId: string;
|
|
16662
|
+
};
|
|
16663
|
+
cookie?: never;
|
|
16664
|
+
};
|
|
16665
|
+
requestBody?: never;
|
|
16666
|
+
responses: {
|
|
16667
|
+
/** @description The plan as a runner needs it */
|
|
16668
|
+
200: {
|
|
16669
|
+
headers: {
|
|
16670
|
+
[name: string]: unknown;
|
|
16671
|
+
};
|
|
16672
|
+
content: {
|
|
16673
|
+
"application/json": components["schemas"]["QualityPlanResponse"];
|
|
16674
|
+
};
|
|
16675
|
+
};
|
|
16676
|
+
/** @description Payload invalid */
|
|
16677
|
+
400: {
|
|
16678
|
+
headers: {
|
|
16679
|
+
[name: string]: unknown;
|
|
16680
|
+
};
|
|
16681
|
+
content?: never;
|
|
16682
|
+
};
|
|
16683
|
+
/** @description Missing or invalid Floor bearer token */
|
|
16684
|
+
401: {
|
|
16685
|
+
headers: {
|
|
16686
|
+
[name: string]: unknown;
|
|
16687
|
+
};
|
|
16688
|
+
content?: never;
|
|
16689
|
+
};
|
|
16690
|
+
/** @description No such plan */
|
|
16691
|
+
404: {
|
|
16692
|
+
headers: {
|
|
16693
|
+
[name: string]: unknown;
|
|
16694
|
+
};
|
|
16695
|
+
content?: never;
|
|
16696
|
+
};
|
|
16697
|
+
};
|
|
16698
|
+
};
|
|
16699
|
+
getQualityPlanDecision: {
|
|
16700
|
+
parameters: {
|
|
16701
|
+
query?: never;
|
|
16702
|
+
header?: never;
|
|
16703
|
+
path: {
|
|
16704
|
+
planId: string;
|
|
16705
|
+
};
|
|
16706
|
+
cookie?: never;
|
|
16707
|
+
};
|
|
16708
|
+
requestBody?: never;
|
|
16709
|
+
responses: {
|
|
16710
|
+
/** @description Evaluation state, block reason while blocked, and the decision once decided */
|
|
16711
|
+
200: {
|
|
16712
|
+
headers: {
|
|
16713
|
+
[name: string]: unknown;
|
|
16714
|
+
};
|
|
16715
|
+
content: {
|
|
16716
|
+
"application/json": components["schemas"]["QualityPlanDecisionResponse"];
|
|
16717
|
+
};
|
|
16718
|
+
};
|
|
16719
|
+
/** @description Payload invalid */
|
|
16720
|
+
400: {
|
|
16721
|
+
headers: {
|
|
16722
|
+
[name: string]: unknown;
|
|
16723
|
+
};
|
|
16724
|
+
content?: never;
|
|
16725
|
+
};
|
|
16726
|
+
/** @description Missing or invalid Floor bearer token */
|
|
16727
|
+
401: {
|
|
16728
|
+
headers: {
|
|
16729
|
+
[name: string]: unknown;
|
|
16730
|
+
};
|
|
16731
|
+
content?: never;
|
|
16732
|
+
};
|
|
16733
|
+
/** @description No such plan */
|
|
16734
|
+
404: {
|
|
16735
|
+
headers: {
|
|
16736
|
+
[name: string]: unknown;
|
|
16737
|
+
};
|
|
16738
|
+
content?: never;
|
|
16739
|
+
};
|
|
16740
|
+
};
|
|
16741
|
+
};
|
|
16742
|
+
recordQualityDeployment: {
|
|
16743
|
+
parameters: {
|
|
16744
|
+
query?: never;
|
|
16745
|
+
header?: never;
|
|
16746
|
+
path?: never;
|
|
16747
|
+
cookie?: never;
|
|
16748
|
+
};
|
|
16749
|
+
requestBody: {
|
|
16750
|
+
content: {
|
|
16751
|
+
"application/json": components["schemas"]["RecordQualityDeploymentRequest"];
|
|
16752
|
+
};
|
|
16753
|
+
};
|
|
16754
|
+
responses: {
|
|
16755
|
+
/** @description The change that verifies the deployment, its plan, and the release references pinned */
|
|
16756
|
+
200: {
|
|
16757
|
+
headers: {
|
|
16758
|
+
[name: string]: unknown;
|
|
16759
|
+
};
|
|
16760
|
+
content: {
|
|
16761
|
+
"application/json": components["schemas"]["RecordQualityDeploymentResponse"];
|
|
16762
|
+
};
|
|
16763
|
+
};
|
|
16764
|
+
/** @description Not an applied backend deployment: nothing was recorded */
|
|
16765
|
+
202: {
|
|
16766
|
+
headers: {
|
|
16767
|
+
[name: string]: unknown;
|
|
16768
|
+
};
|
|
16769
|
+
content: {
|
|
16770
|
+
"application/json": components["schemas"]["QualityDeploymentIgnored"];
|
|
16771
|
+
};
|
|
16772
|
+
};
|
|
16773
|
+
/** @description Payload invalid */
|
|
16774
|
+
400: {
|
|
16775
|
+
headers: {
|
|
16776
|
+
[name: string]: unknown;
|
|
16777
|
+
};
|
|
16778
|
+
content?: never;
|
|
16779
|
+
};
|
|
16780
|
+
/** @description Missing or invalid Floor bearer token */
|
|
16781
|
+
401: {
|
|
16782
|
+
headers: {
|
|
16783
|
+
[name: string]: unknown;
|
|
16784
|
+
};
|
|
16785
|
+
content?: never;
|
|
16786
|
+
};
|
|
16787
|
+
};
|
|
16788
|
+
};
|
|
16426
16789
|
startImpersonation: {
|
|
16427
16790
|
parameters: {
|
|
16428
16791
|
query?: never;
|
package/src/decisions/README.md
CHANGED
|
@@ -69,6 +69,11 @@ decision-effect-is-a-domain-parameter.
|
|
|
69
69
|
effect and no apply-protocol address; `questionId` is its only id. Its `kind`
|
|
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
|
+
- **A matrix keeps its state on rows.** A presentation with `rows` is a matrix:
|
|
73
|
+
each row is one subject answered with one of `choices` (the columns). Columns
|
|
74
|
+
carry no `selected`/`recommended`; each row holds its own, within its
|
|
75
|
+
`allowedChoiceIds`. One answer per row, no free text. Enforced by
|
|
76
|
+
`matrixPresentationDefect` inside the schema (ADR-CONTRACTS-213).
|
|
72
77
|
|
|
73
78
|
## Measured evidence is optional
|
|
74
79
|
|
|
@@ -11,6 +11,7 @@ import { describe, expect, it } from "vitest";
|
|
|
11
11
|
import {
|
|
12
12
|
DecisionChoicePresentationSchema,
|
|
13
13
|
DecisionQuestionPresentationSchema,
|
|
14
|
+
matrixPresentationDefect,
|
|
14
15
|
} from "../index";
|
|
15
16
|
|
|
16
17
|
function presentedQuestion(overrides: Record<string, unknown> = {}) {
|
|
@@ -110,3 +111,155 @@ describe("DecisionQuestionPresentationSchema", () => {
|
|
|
110
111
|
);
|
|
111
112
|
});
|
|
112
113
|
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A MATRIX question: one subject per row, each answered with one of the
|
|
117
|
+
* question's choices. Rows carry the state; the columns carry none.
|
|
118
|
+
*/
|
|
119
|
+
function matrixQuestion(overrides: Record<string, unknown> = {}) {
|
|
120
|
+
return presentedQuestion({
|
|
121
|
+
questionId: "treatment_matrix:s1",
|
|
122
|
+
kind: "treatment_matrix",
|
|
123
|
+
header: "Strategy memo",
|
|
124
|
+
question:
|
|
125
|
+
"Plan was found relevant to 2 places. How should it be used in each?",
|
|
126
|
+
choices: [
|
|
127
|
+
{
|
|
128
|
+
choiceId: "attached",
|
|
129
|
+
label: "Attach",
|
|
130
|
+
recommended: false,
|
|
131
|
+
selected: false,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
choiceId: "direction_only",
|
|
135
|
+
label: "Direction only",
|
|
136
|
+
recommended: false,
|
|
137
|
+
selected: false,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
choiceId: "excluded",
|
|
141
|
+
label: "Leave out",
|
|
142
|
+
recommended: false,
|
|
143
|
+
selected: false,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
multiSelect: false,
|
|
147
|
+
freeFormAllowed: false,
|
|
148
|
+
recommendation: { strength: "confident" },
|
|
149
|
+
rows: [
|
|
150
|
+
{
|
|
151
|
+
rowId: "item-1",
|
|
152
|
+
label: "Support",
|
|
153
|
+
note: "Shared more narrowly than this doc",
|
|
154
|
+
evidence: [],
|
|
155
|
+
selectedChoiceId: null,
|
|
156
|
+
recommendedChoiceId: "direction_only",
|
|
157
|
+
allowedChoiceIds: ["attached", "direction_only", "excluded"],
|
|
158
|
+
required: true,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
rowId: "item-2",
|
|
162
|
+
label: "Engineering",
|
|
163
|
+
evidence: [
|
|
164
|
+
{ label: "Why it was found", value: "Title search", withheld: false },
|
|
165
|
+
],
|
|
166
|
+
selectedChoiceId: "attached",
|
|
167
|
+
recommendedChoiceId: null,
|
|
168
|
+
allowedChoiceIds: ["attached", "direction_only", "excluded"],
|
|
169
|
+
required: false,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
...overrides,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
describe("DecisionQuestionPresentationSchema — matrix", () => {
|
|
177
|
+
it("accepts a matrix whose rows hold their own selection and recommendation", () => {
|
|
178
|
+
const parsed =
|
|
179
|
+
DecisionQuestionPresentationSchema.safeParse(matrixQuestion());
|
|
180
|
+
expect(parsed.success).toBe(true);
|
|
181
|
+
expect(parsed.data?.rows).toHaveLength(2);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("leaves a question without rows untouched", () => {
|
|
185
|
+
expect(
|
|
186
|
+
matrixPresentationDefect(
|
|
187
|
+
DecisionQuestionPresentationSchema.parse(presentedQuestion()),
|
|
188
|
+
),
|
|
189
|
+
).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it.each([
|
|
193
|
+
["matrix_multi_select", { multiSelect: true }],
|
|
194
|
+
["matrix_free_form", { freeFormAllowed: true }],
|
|
195
|
+
["matrix_recommendation_mismatch", { recommendation: null }],
|
|
196
|
+
])("refuses %s", (defect, overrides) => {
|
|
197
|
+
const parsed = DecisionQuestionPresentationSchema.safeParse(
|
|
198
|
+
matrixQuestion(overrides),
|
|
199
|
+
);
|
|
200
|
+
expect(parsed.success).toBe(false);
|
|
201
|
+
expect(parsed.error?.issues[0]?.message).toBe(defect);
|
|
202
|
+
expect(parsed.error?.issues[0]?.path).toEqual(["rows"]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("refuses state on a column", () => {
|
|
206
|
+
const base = matrixQuestion();
|
|
207
|
+
const choices = base.choices.map((c, i) =>
|
|
208
|
+
i === 0 ? { ...c, selected: true } : c,
|
|
209
|
+
);
|
|
210
|
+
const parsed = DecisionQuestionPresentationSchema.safeParse({
|
|
211
|
+
...base,
|
|
212
|
+
choices,
|
|
213
|
+
});
|
|
214
|
+
expect(parsed.error?.issues[0]?.message).toBe("matrix_column_state");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("refuses two rows with one id", () => {
|
|
218
|
+
const base = matrixQuestion();
|
|
219
|
+
const rows = [base.rows[0], { ...base.rows[1], rowId: "item-1" }];
|
|
220
|
+
const parsed = DecisionQuestionPresentationSchema.safeParse({
|
|
221
|
+
...base,
|
|
222
|
+
rows,
|
|
223
|
+
});
|
|
224
|
+
expect(parsed.error?.issues[0]?.message).toBe("matrix_duplicate_row");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("refuses a row allowing a choice that is not a column", () => {
|
|
228
|
+
const base = matrixQuestion();
|
|
229
|
+
const rows = [
|
|
230
|
+
{ ...base.rows[0], allowedChoiceIds: ["attached", "archived"] },
|
|
231
|
+
base.rows[1],
|
|
232
|
+
];
|
|
233
|
+
const parsed = DecisionQuestionPresentationSchema.safeParse({
|
|
234
|
+
...base,
|
|
235
|
+
rows,
|
|
236
|
+
});
|
|
237
|
+
expect(parsed.error?.issues[0]?.message).toBe("matrix_unknown_choice");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("refuses a selection or recommendation the row does not allow", () => {
|
|
241
|
+
const base = matrixQuestion();
|
|
242
|
+
const narrowed = { ...base.rows[1], allowedChoiceIds: ["excluded"] };
|
|
243
|
+
const selected = DecisionQuestionPresentationSchema.safeParse({
|
|
244
|
+
...base,
|
|
245
|
+
rows: [base.rows[0], narrowed],
|
|
246
|
+
});
|
|
247
|
+
expect(selected.error?.issues[0]?.message).toBe(
|
|
248
|
+
"matrix_selection_not_allowed",
|
|
249
|
+
);
|
|
250
|
+
const recommended = DecisionQuestionPresentationSchema.safeParse({
|
|
251
|
+
...base,
|
|
252
|
+
rows: [{ ...base.rows[0], allowedChoiceIds: ["excluded"] }, base.rows[1]],
|
|
253
|
+
});
|
|
254
|
+
expect(recommended.error?.issues[0]?.message).toBe(
|
|
255
|
+
"matrix_recommendation_not_allowed",
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("refuses an empty row list", () => {
|
|
260
|
+
const parsed = DecisionQuestionPresentationSchema.safeParse(
|
|
261
|
+
matrixQuestion({ rows: [] }),
|
|
262
|
+
);
|
|
263
|
+
expect(parsed.success).toBe(false);
|
|
264
|
+
});
|
|
265
|
+
});
|
package/src/decisions/index.ts
CHANGED
|
@@ -47,10 +47,15 @@ export {
|
|
|
47
47
|
decisionAnswerSchemaOf,
|
|
48
48
|
} from "./schemas";
|
|
49
49
|
|
|
50
|
-
export type {
|
|
50
|
+
export type {
|
|
51
|
+
DecisionMatrixRowPresentation,
|
|
52
|
+
DecisionQuestionPresentation,
|
|
53
|
+
} from "./presentation";
|
|
51
54
|
|
|
52
55
|
export {
|
|
53
56
|
DecisionChoicePresentationSchema,
|
|
54
57
|
DecisionEvidenceRowSchema,
|
|
58
|
+
DecisionMatrixRowPresentationSchema,
|
|
55
59
|
DecisionQuestionPresentationSchema,
|
|
60
|
+
matrixPresentationDefect,
|
|
56
61
|
} from "./presentation";
|