@gajae-code/coding-agent 0.11.8 → 0.11.9
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/CHANGELOG.md +18 -1
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
- package/dist/types/gjc-runtime/state-writer.d.ts +44 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/session/internal/managed-session-scope.d.ts +12 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -3
- package/dist/types/session/session-manager.d.ts +5 -1
- package/dist/types/session/session-storage.d.ts +13 -0
- package/dist/types/skill-state/active-state.d.ts +4 -0
- package/package.json +8 -8
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli.ts +32 -0
- package/src/commands/deep-interview.ts +9 -4
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/model-profiles.ts +2 -3
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +78 -163
- package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
- package/src/gjc-runtime/deep-interview-draft.ts +842 -0
- package/src/gjc-runtime/deep-interview-payload.ts +262 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
- package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
- package/src/gjc-runtime/deep-interview-state.ts +916 -4
- package/src/gjc-runtime/ralplan-runtime.ts +3 -1
- package/src/gjc-runtime/state-writer.ts +166 -2
- package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
- package/src/gjc-runtime/workflow-manifest.ts +191 -5
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/modes/components/welcome.ts +10 -2
- package/src/modes/interactive-mode.ts +29 -9
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/runtime-mcp/json-rpc.ts +7 -4
- package/src/runtime-mcp/redaction.ts +30 -0
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-control.ts +5 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/bus/telegram-daemon.ts +18 -2
- package/src/session/agent-session.ts +86 -0
- package/src/session/internal/managed-session-scope.ts +206 -111
- package/src/session/internal/managed-session-storage.ts +17 -4
- package/src/session/session-manager.ts +96 -33
- package/src/session/session-storage.ts +23 -0
- package/src/skill-state/active-state.ts +10 -0
- package/src/task/executor.ts +13 -5
- package/src/tools/ask.ts +1 -0
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
clampReportedAmbiguity,
|
|
4
|
+
computeAmbiguityFloor,
|
|
5
|
+
type DeepInterviewAmbiguityMilestone,
|
|
6
|
+
deriveAmbiguityMilestone,
|
|
7
|
+
scoreToUnits,
|
|
8
|
+
weightedAmbiguityUnits,
|
|
9
|
+
} from "./deep-interview-ambiguity";
|
|
2
10
|
|
|
3
11
|
/**
|
|
4
12
|
* Pure, dependency-free foundation for deep-interview state shape.
|
|
@@ -46,10 +54,6 @@ export interface DeepInterviewTriggerMetadata {
|
|
|
46
54
|
status: DeepInterviewTriggerStatus;
|
|
47
55
|
component: string;
|
|
48
56
|
dimension: string;
|
|
49
|
-
priorDimensionScore?: number;
|
|
50
|
-
newDimensionScore?: number;
|
|
51
|
-
priorAmbiguity?: number;
|
|
52
|
-
newAmbiguity?: number;
|
|
53
57
|
evidence?: string;
|
|
54
58
|
contradictedFactId?: string;
|
|
55
59
|
/** Required when status is `disputed` or `unresolved` to exempt the invariant. */
|
|
@@ -80,10 +84,12 @@ export interface DeepInterviewRoundRecord {
|
|
|
80
84
|
/** Deterministic floor in effect when this round was scored, when it clamped. */
|
|
81
85
|
ambiguity_floor?: number;
|
|
82
86
|
triggers?: DeepInterviewTriggerMetadata[];
|
|
87
|
+
round_result_digest?: string;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
export interface DeepInterviewStateEnvelope {
|
|
86
91
|
threshold?: number;
|
|
92
|
+
threshold_units?: number;
|
|
87
93
|
threshold_source?: string;
|
|
88
94
|
state?: Record<string, unknown>;
|
|
89
95
|
[key: string]: unknown;
|
|
@@ -162,6 +168,7 @@ const HOISTED_STATE_FIELDS = [
|
|
|
162
168
|
"type",
|
|
163
169
|
"language",
|
|
164
170
|
"threshold",
|
|
171
|
+
"threshold_units",
|
|
165
172
|
"threshold_source",
|
|
166
173
|
] as const;
|
|
167
174
|
|
|
@@ -800,3 +807,908 @@ export function assertDeepInterviewIntentReview(
|
|
|
800
807
|
)
|
|
801
808
|
throw new Error("intent review approval evidence is invalid");
|
|
802
809
|
}
|
|
810
|
+
export type DeepInterviewDimension = "goal" | "constraints" | "criteria" | "context";
|
|
811
|
+
export type DeepInterviewResolution =
|
|
812
|
+
| "auto_research_accepted"
|
|
813
|
+
| "auto_answer"
|
|
814
|
+
| "direct"
|
|
815
|
+
| "refined"
|
|
816
|
+
| "cited_confirmation";
|
|
817
|
+
|
|
818
|
+
export interface DeepInterviewFactOperation {
|
|
819
|
+
op: "add" | "dispute" | "supersede";
|
|
820
|
+
id: string;
|
|
821
|
+
statement?: string;
|
|
822
|
+
component?: string;
|
|
823
|
+
dimension?: DeepInterviewDimension;
|
|
824
|
+
evidence?: string;
|
|
825
|
+
target_id?: string;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
export interface DeepInterviewOntologyEntity {
|
|
829
|
+
id: string;
|
|
830
|
+
name: string;
|
|
831
|
+
type: string;
|
|
832
|
+
fields: string[];
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export interface DeepInterviewOntologyRelationship {
|
|
836
|
+
id: string;
|
|
837
|
+
from_entity_id: string;
|
|
838
|
+
to_entity_id: string;
|
|
839
|
+
type: string;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export interface DeepInterviewOntologyReasoning {
|
|
843
|
+
statement: string;
|
|
844
|
+
evidence?: string;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
export interface DeepInterviewOntologyInput {
|
|
848
|
+
entities: DeepInterviewOntologyEntity[];
|
|
849
|
+
relationships: DeepInterviewOntologyRelationship[];
|
|
850
|
+
reasoning: DeepInterviewOntologyReasoning[];
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export interface DeepInterviewComponentUpdate {
|
|
854
|
+
component_id: string;
|
|
855
|
+
scores: Record<DeepInterviewDimension, number>;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Optional legacy assertions accepted only when they agree with the native
|
|
860
|
+
* target. The persisted target is always derived from component scores.
|
|
861
|
+
*/
|
|
862
|
+
export interface DeepInterviewTargetingInput {
|
|
863
|
+
target_component_id?: string;
|
|
864
|
+
target_dimension?: DeepInterviewDimension;
|
|
865
|
+
weakest_component_id?: string;
|
|
866
|
+
weakest_dimension?: DeepInterviewDimension;
|
|
867
|
+
last_targeted_component_id?: string | null;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
export interface DeepInterviewRoundBookkeeping {
|
|
871
|
+
resolution: DeepInterviewResolution;
|
|
872
|
+
round_ids?: string[];
|
|
873
|
+
counter_deltas?: Record<string, number>;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Closed evidence submitted for one scoring transaction. All convergence values
|
|
878
|
+
* (ambiguity, floor, milestone, ontology counts, rotation, and streak) are
|
|
879
|
+
* native-derived and intentionally cannot be supplied here.
|
|
880
|
+
*/
|
|
881
|
+
export interface DeepInterviewRoundResultV1 {
|
|
882
|
+
global_scores: Record<DeepInterviewDimension, number>;
|
|
883
|
+
component_updates?: DeepInterviewComponentUpdate[];
|
|
884
|
+
targeting?: DeepInterviewTargetingInput;
|
|
885
|
+
triggers?: DeepInterviewTriggerMetadata[];
|
|
886
|
+
fact_ops?: DeepInterviewFactOperation[];
|
|
887
|
+
ontology?: DeepInterviewOntologyInput | DeepInterviewOntologyEntity[];
|
|
888
|
+
bookkeeping?: DeepInterviewRoundBookkeeping;
|
|
889
|
+
/** Compatibility adapter for recorder callers; never persisted as the v1 wire shape. */
|
|
890
|
+
component_scores?: Record<string, Record<DeepInterviewDimension, number>>;
|
|
891
|
+
auto_answered?: boolean;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Version 1 native projection returned by `apply-round-result` as
|
|
896
|
+
* `native_projection`. This is the complete renderable convergence surface;
|
|
897
|
+
* callers must not derive or supplement it from candidate scorer input.
|
|
898
|
+
*/
|
|
899
|
+
export interface DeepInterviewRoundResultProjection {
|
|
900
|
+
score_units: Partial<Record<DeepInterviewDimension, number>>;
|
|
901
|
+
weighted_ambiguity: number;
|
|
902
|
+
weighted_ambiguity_units: number;
|
|
903
|
+
floor: number;
|
|
904
|
+
floor_units: number;
|
|
905
|
+
floor_cause: ReturnType<typeof computeAmbiguityFloor>;
|
|
906
|
+
effective_ambiguity: number;
|
|
907
|
+
effective_ambiguity_units: number;
|
|
908
|
+
prior_effective_ambiguity: number | null;
|
|
909
|
+
direction: "increased" | "decreased" | "unchanged" | "initial";
|
|
910
|
+
ambiguity_milestone: DeepInterviewAmbiguityMilestone;
|
|
911
|
+
topology: unknown;
|
|
912
|
+
topology_counts: { active: number; deferred: number; total: number };
|
|
913
|
+
ontology: unknown;
|
|
914
|
+
ontology_counts: { stable: number; changed: number; new: number; basis: string };
|
|
915
|
+
targeting: {
|
|
916
|
+
target_component_id: string | null;
|
|
917
|
+
target_dimension: DeepInterviewDimension | null;
|
|
918
|
+
last_targeted_component_id: string | null;
|
|
919
|
+
};
|
|
920
|
+
transition: {
|
|
921
|
+
round_key: string;
|
|
922
|
+
lifecycle: "scored";
|
|
923
|
+
auto_answer_streak: number;
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export type DeepInterviewApplyRoundResult =
|
|
928
|
+
| { kind: "noop"; envelope: DeepInterviewStateEnvelope; projection?: DeepInterviewRoundResultProjection }
|
|
929
|
+
| { kind: "write"; envelope: DeepInterviewStateEnvelope; projection: DeepInterviewRoundResultProjection };
|
|
930
|
+
|
|
931
|
+
function canonicalJsonValue(value: unknown): unknown {
|
|
932
|
+
if (Array.isArray(value)) return value.map(canonicalJsonValue);
|
|
933
|
+
if (!isPlainObject(value)) {
|
|
934
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
935
|
+
throw new TypeError("canonical JSON rejects non-finite numbers");
|
|
936
|
+
}
|
|
937
|
+
if (value === undefined) throw new TypeError("canonical JSON rejects undefined");
|
|
938
|
+
return value;
|
|
939
|
+
}
|
|
940
|
+
const output: Record<string, unknown> = {};
|
|
941
|
+
for (const key of Object.keys(value).sort()) output[key] = canonicalJsonValue(value[key]);
|
|
942
|
+
return output;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Strict UTF-8 JSON serialization used by v1 replay digests. */
|
|
946
|
+
export function canonicalDeepInterviewJson(value: unknown): string {
|
|
947
|
+
return JSON.stringify(canonicalJsonValue(value));
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
export function deepInterviewRoundResultDigest(input: {
|
|
951
|
+
round: number;
|
|
952
|
+
question_id: string;
|
|
953
|
+
round_id?: string | null;
|
|
954
|
+
result: unknown;
|
|
955
|
+
}): string {
|
|
956
|
+
return createHash("sha256")
|
|
957
|
+
.update(
|
|
958
|
+
canonicalDeepInterviewJson({
|
|
959
|
+
v: 1,
|
|
960
|
+
round: input.round,
|
|
961
|
+
question_id: input.question_id,
|
|
962
|
+
round_id: input.round_id ?? null,
|
|
963
|
+
result: input.result,
|
|
964
|
+
}),
|
|
965
|
+
)
|
|
966
|
+
.digest("hex");
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** Complete persisted answer identity; omitted optional values normalize to null. */
|
|
970
|
+
export function deepInterviewAnswerIdentityEqual(
|
|
971
|
+
a: Pick<
|
|
972
|
+
DeepInterviewRoundRecord,
|
|
973
|
+
| "round"
|
|
974
|
+
| "round_key"
|
|
975
|
+
| "round_id"
|
|
976
|
+
| "question_id"
|
|
977
|
+
| "component"
|
|
978
|
+
| "dimension"
|
|
979
|
+
| "question_text"
|
|
980
|
+
| "question_hash"
|
|
981
|
+
| "selected_options"
|
|
982
|
+
| "custom_input"
|
|
983
|
+
>,
|
|
984
|
+
b: Pick<
|
|
985
|
+
DeepInterviewRoundRecord,
|
|
986
|
+
| "round"
|
|
987
|
+
| "round_key"
|
|
988
|
+
| "round_id"
|
|
989
|
+
| "question_id"
|
|
990
|
+
| "component"
|
|
991
|
+
| "dimension"
|
|
992
|
+
| "question_text"
|
|
993
|
+
| "question_hash"
|
|
994
|
+
| "selected_options"
|
|
995
|
+
| "custom_input"
|
|
996
|
+
>,
|
|
997
|
+
): boolean {
|
|
998
|
+
return (
|
|
999
|
+
canonicalDeepInterviewJson({
|
|
1000
|
+
round: a.round,
|
|
1001
|
+
round_key: a.round_key,
|
|
1002
|
+
round_id: a.round_id ?? null,
|
|
1003
|
+
question_id: a.question_id ?? null,
|
|
1004
|
+
component: a.component ?? null,
|
|
1005
|
+
dimension: a.dimension ?? null,
|
|
1006
|
+
question_text: a.question_text ?? null,
|
|
1007
|
+
question_hash: a.question_hash,
|
|
1008
|
+
selected_options: a.selected_options ?? [],
|
|
1009
|
+
custom_input: a.custom_input ?? null,
|
|
1010
|
+
}) ===
|
|
1011
|
+
canonicalDeepInterviewJson({
|
|
1012
|
+
round: b.round,
|
|
1013
|
+
round_key: b.round_key,
|
|
1014
|
+
round_id: b.round_id ?? null,
|
|
1015
|
+
question_id: b.question_id ?? null,
|
|
1016
|
+
component: b.component ?? null,
|
|
1017
|
+
dimension: b.dimension ?? null,
|
|
1018
|
+
question_text: b.question_text ?? null,
|
|
1019
|
+
question_hash: b.question_hash,
|
|
1020
|
+
selected_options: b.selected_options ?? [],
|
|
1021
|
+
custom_input: b.custom_input ?? null,
|
|
1022
|
+
})
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** Apply one closed v1 scoring round. The input contains evidence only; all convergence state is derived here. */
|
|
1027
|
+
export function applyDeepInterviewRoundResultV1(
|
|
1028
|
+
envelopeValue: unknown,
|
|
1029
|
+
roundKey: string,
|
|
1030
|
+
result: DeepInterviewRoundResultV1,
|
|
1031
|
+
now: string,
|
|
1032
|
+
): DeepInterviewApplyRoundResult {
|
|
1033
|
+
const envelope = normalizeDeepInterviewEnvelope(envelopeValue);
|
|
1034
|
+
const state = { ...(envelope.state ?? {}) };
|
|
1035
|
+
const rounds = asRecordArray(state.rounds) as unknown as DeepInterviewRoundRecord[];
|
|
1036
|
+
const index = rounds.findIndex(round => round.round_key === roundKey);
|
|
1037
|
+
if (index < 0) throw new Error("DI_ROUND_NOT_FOUND");
|
|
1038
|
+
const shell = rounds[index];
|
|
1039
|
+
if (shell.lifecycle !== "answered" && shell.lifecycle !== "pending_scoring" && shell.lifecycle !== "scored")
|
|
1040
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1041
|
+
if (typeof shell.question_id !== "string" || shell.question_id === "") throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1042
|
+
const digest = deepInterviewRoundResultDigest({
|
|
1043
|
+
round: shell.round,
|
|
1044
|
+
question_id: shell.question_id,
|
|
1045
|
+
round_id: shell.round_id ?? null,
|
|
1046
|
+
result,
|
|
1047
|
+
});
|
|
1048
|
+
if (shell.lifecycle === "scored") {
|
|
1049
|
+
if (shell.round_result_digest !== digest) throw new Error("DI_ROUND_RESULT_CONFLICT");
|
|
1050
|
+
return { kind: "noop", envelope };
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const type = state.type;
|
|
1054
|
+
if (type !== "greenfield" && type !== "brownfield") throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1055
|
+
const dimensions: DeepInterviewDimension[] =
|
|
1056
|
+
type === "brownfield" ? ["goal", "constraints", "criteria", "context"] : ["goal", "constraints", "criteria"];
|
|
1057
|
+
const componentScores =
|
|
1058
|
+
result.component_scores ??
|
|
1059
|
+
Object.fromEntries((result.component_updates ?? []).map(update => [update.component_id, update.scores]));
|
|
1060
|
+
const finiteScore = (value: unknown): value is number =>
|
|
1061
|
+
typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
1062
|
+
if (!dimensions.every(dimension => finiteScore(result.global_scores[dimension])))
|
|
1063
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1064
|
+
if (
|
|
1065
|
+
rounds.some(
|
|
1066
|
+
round =>
|
|
1067
|
+
round.lifecycle !== "scored" &&
|
|
1068
|
+
(round.round < shell.round || (round.round === shell.round && round.round_key < shell.round_key)),
|
|
1069
|
+
)
|
|
1070
|
+
)
|
|
1071
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1072
|
+
|
|
1073
|
+
const topology = isPlainObject(state.topology) ? state.topology : undefined;
|
|
1074
|
+
const activeComponents =
|
|
1075
|
+
topology && Array.isArray(topology.components)
|
|
1076
|
+
? topology.components.filter(
|
|
1077
|
+
component => isPlainObject(component) && component.active !== false && component.status !== "deferred",
|
|
1078
|
+
)
|
|
1079
|
+
: [];
|
|
1080
|
+
if (activeComponents.length > 0) {
|
|
1081
|
+
for (const component of activeComponents) {
|
|
1082
|
+
const id = component.id;
|
|
1083
|
+
if (
|
|
1084
|
+
typeof id !== "string" ||
|
|
1085
|
+
!isPlainObject(componentScores[id]) ||
|
|
1086
|
+
!dimensions.every(d => finiteScore(componentScores[id][d]))
|
|
1087
|
+
)
|
|
1088
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1089
|
+
}
|
|
1090
|
+
for (const dimension of dimensions) {
|
|
1091
|
+
const weakest = Math.min(
|
|
1092
|
+
...activeComponents.map(component => componentScores[String(component.id)][dimension]),
|
|
1093
|
+
);
|
|
1094
|
+
if (result.global_scores[dimension] !== weakest) throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const facts = asRecordArray(state.established_facts);
|
|
1098
|
+
for (const operation of result.fact_ops ?? []) {
|
|
1099
|
+
if (!operation || typeof operation.id !== "string" || operation.id === "")
|
|
1100
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1101
|
+
const factIndex = facts.findIndex(fact => fact.id === operation.id);
|
|
1102
|
+
if (operation.op === "add") {
|
|
1103
|
+
if (!operation.statement || factIndex >= 0) throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1104
|
+
facts.push({
|
|
1105
|
+
id: operation.id,
|
|
1106
|
+
statement: operation.statement,
|
|
1107
|
+
round: shell.round,
|
|
1108
|
+
component: operation.component,
|
|
1109
|
+
dimension: operation.dimension,
|
|
1110
|
+
evidence: operation.evidence,
|
|
1111
|
+
disputed: false,
|
|
1112
|
+
});
|
|
1113
|
+
} else if (operation.op === "dispute") {
|
|
1114
|
+
if (factIndex < 0) throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1115
|
+
facts[factIndex] = { ...facts[factIndex], disputed: true };
|
|
1116
|
+
} else {
|
|
1117
|
+
if (factIndex < 0 || !operation.target_id || !facts.some(fact => fact.id === operation.target_id))
|
|
1118
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1119
|
+
facts[factIndex] = { ...facts[factIndex], disputed: true, superseded_by: operation.target_id };
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
for (const trigger of result.triggers ?? []) {
|
|
1123
|
+
if (
|
|
1124
|
+
!trigger ||
|
|
1125
|
+
!["A", "B", "C", "D"].includes(trigger.kind) ||
|
|
1126
|
+
!["active", "disputed", "unresolved"].includes(trigger.status) ||
|
|
1127
|
+
typeof trigger.name !== "string" ||
|
|
1128
|
+
trigger.name === "" ||
|
|
1129
|
+
typeof trigger.component !== "string" ||
|
|
1130
|
+
trigger.component === "" ||
|
|
1131
|
+
!dimensions.includes(trigger.dimension as DeepInterviewDimension)
|
|
1132
|
+
)
|
|
1133
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1134
|
+
if ((trigger.status === "disputed" || trigger.status === "unresolved") && !trigger.rationale)
|
|
1135
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1136
|
+
if (trigger.contradictedFactId && !facts.some(fact => fact.id === trigger.contradictedFactId))
|
|
1137
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1138
|
+
if (
|
|
1139
|
+
topology &&
|
|
1140
|
+
Array.isArray(topology.components) &&
|
|
1141
|
+
topology.components.length > 0 &&
|
|
1142
|
+
!topology.components.some(component => isPlainObject(component) && component.id === trigger.component)
|
|
1143
|
+
)
|
|
1144
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1145
|
+
}
|
|
1146
|
+
const priorSnapshot = Array.isArray(state.ontology_snapshots) ? state.ontology_snapshots.at(-1) : undefined;
|
|
1147
|
+
const priorEntities =
|
|
1148
|
+
isPlainObject(priorSnapshot) && Array.isArray(priorSnapshot.entities)
|
|
1149
|
+
? priorSnapshot.entities.filter(isPlainObject)
|
|
1150
|
+
: [];
|
|
1151
|
+
const ontologyInput = result.ontology;
|
|
1152
|
+
const entities = (Array.isArray(ontologyInput) ? ontologyInput : (ontologyInput?.entities ?? [])).map(entity => ({
|
|
1153
|
+
...entity,
|
|
1154
|
+
relationships: Array.isArray(ontologyInput)
|
|
1155
|
+
? []
|
|
1156
|
+
: (ontologyInput?.relationships ?? [])
|
|
1157
|
+
.filter(
|
|
1158
|
+
relationship => relationship.from_entity_id === entity.id || relationship.to_entity_id === entity.id,
|
|
1159
|
+
)
|
|
1160
|
+
.map(relationship => relationship.id),
|
|
1161
|
+
}));
|
|
1162
|
+
if (
|
|
1163
|
+
!entities.every(
|
|
1164
|
+
entity =>
|
|
1165
|
+
entity &&
|
|
1166
|
+
typeof entity.id === "string" &&
|
|
1167
|
+
typeof entity.name === "string" &&
|
|
1168
|
+
entity.name !== "" &&
|
|
1169
|
+
typeof entity.type === "string" &&
|
|
1170
|
+
Array.isArray(entity.fields) &&
|
|
1171
|
+
Array.isArray(entity.relationships),
|
|
1172
|
+
)
|
|
1173
|
+
)
|
|
1174
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1175
|
+
if (isPlainObject(priorSnapshot)) {
|
|
1176
|
+
const priorSnapshotRound = priorSnapshot.round;
|
|
1177
|
+
if (
|
|
1178
|
+
typeof priorSnapshotRound !== "number" ||
|
|
1179
|
+
!Number.isSafeInteger(priorSnapshotRound) ||
|
|
1180
|
+
priorSnapshotRound >= shell.round
|
|
1181
|
+
)
|
|
1182
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1183
|
+
}
|
|
1184
|
+
const basis = entities.length === 0 ? "no_entities" : priorSnapshot === undefined ? "first_round" : "compared";
|
|
1185
|
+
const unmatchedPrior = new Set(priorEntities.map((_, index) => index));
|
|
1186
|
+
const stableMatches = new Set<number>();
|
|
1187
|
+
const changedMatches = new Set<number>();
|
|
1188
|
+
if (basis === "compared") {
|
|
1189
|
+
for (let current = 0; current < entities.length; current += 1) {
|
|
1190
|
+
const prior = priorEntities.findIndex(
|
|
1191
|
+
(entity, index) =>
|
|
1192
|
+
unmatchedPrior.has(index) &&
|
|
1193
|
+
entity.name === entities[current].name &&
|
|
1194
|
+
entity.type === entities[current].type,
|
|
1195
|
+
);
|
|
1196
|
+
if (prior >= 0) {
|
|
1197
|
+
unmatchedPrior.delete(prior);
|
|
1198
|
+
stableMatches.add(current);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const candidates: { prior: number; current: number }[] = [];
|
|
1202
|
+
for (let current = 0; current < entities.length; current += 1) {
|
|
1203
|
+
if (stableMatches.has(current)) continue;
|
|
1204
|
+
const fields = new Set(entities[current].fields);
|
|
1205
|
+
for (const prior of unmatchedPrior) {
|
|
1206
|
+
if (priorEntities[prior].type !== entities[current].type) continue;
|
|
1207
|
+
const priorFields = new Set(priorEntities[prior].fields as unknown[]);
|
|
1208
|
+
const overlap = [...fields].filter(field => priorFields.has(field)).length;
|
|
1209
|
+
if (overlap * 2 > Math.max(fields.size, priorFields.size)) candidates.push({ prior, current });
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
candidates.sort(
|
|
1213
|
+
(a, b) =>
|
|
1214
|
+
String(priorEntities[a.prior].id).localeCompare(String(priorEntities[b.prior].id)) ||
|
|
1215
|
+
String(entities[a.current].id).localeCompare(String(entities[b.current].id)),
|
|
1216
|
+
);
|
|
1217
|
+
const matchedCurrent = new Set<number>();
|
|
1218
|
+
for (const candidate of candidates) {
|
|
1219
|
+
if (unmatchedPrior.has(candidate.prior) && !matchedCurrent.has(candidate.current)) {
|
|
1220
|
+
unmatchedPrior.delete(candidate.prior);
|
|
1221
|
+
matchedCurrent.add(candidate.current);
|
|
1222
|
+
changedMatches.add(candidate.current);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
const stable = stableMatches.size;
|
|
1227
|
+
const changed = changedMatches.size;
|
|
1228
|
+
const fresh = entities.length - stable - changed;
|
|
1229
|
+
const stabilityRatio =
|
|
1230
|
+
basis === "compared" ? Math.floor(((stable + changed) * 10_000) / entities.length + 0.5) / 10_000 : null;
|
|
1231
|
+
const ontologySnapshots = [
|
|
1232
|
+
...(Array.isArray(state.ontology_snapshots) ? state.ontology_snapshots : []),
|
|
1233
|
+
{
|
|
1234
|
+
round: shell.round,
|
|
1235
|
+
captured_at: now,
|
|
1236
|
+
entities,
|
|
1237
|
+
basis,
|
|
1238
|
+
stable_entities: stable,
|
|
1239
|
+
new_entities: fresh,
|
|
1240
|
+
changed_entities: changed,
|
|
1241
|
+
stability_ratio: stabilityRatio,
|
|
1242
|
+
},
|
|
1243
|
+
];
|
|
1244
|
+
|
|
1245
|
+
const nextTopology =
|
|
1246
|
+
topology && Array.isArray(topology.components)
|
|
1247
|
+
? {
|
|
1248
|
+
...topology,
|
|
1249
|
+
components: topology.components.map(component => {
|
|
1250
|
+
if (!isPlainObject(component) || typeof component.id !== "string") return component;
|
|
1251
|
+
const scores = componentScores[component.id];
|
|
1252
|
+
return scores ? { ...component, clarity_scores: { ...scores } } : component;
|
|
1253
|
+
}),
|
|
1254
|
+
}
|
|
1255
|
+
: state.topology;
|
|
1256
|
+
const activeIds = activeComponents.map(component => String(component.id));
|
|
1257
|
+
const componentWeakness = (id: string) => Math.min(...dimensions.map(dimension => componentScores[id][dimension]));
|
|
1258
|
+
const weakestValue = activeIds.length === 0 ? undefined : Math.min(...activeIds.map(componentWeakness));
|
|
1259
|
+
const tiedWeakest = activeIds.filter(id => componentWeakness(id) === weakestValue);
|
|
1260
|
+
const previousTarget =
|
|
1261
|
+
isPlainObject(topology) && typeof topology.last_targeted_component_id === "string"
|
|
1262
|
+
? topology.last_targeted_component_id
|
|
1263
|
+
: null;
|
|
1264
|
+
const rotatedCandidates =
|
|
1265
|
+
tiedWeakest.length > 1 && previousTarget !== null
|
|
1266
|
+
? [...tiedWeakest].sort(
|
|
1267
|
+
(a, b) =>
|
|
1268
|
+
((activeIds.indexOf(a) - activeIds.indexOf(previousTarget) + activeIds.length) % activeIds.length) -
|
|
1269
|
+
((activeIds.indexOf(b) - activeIds.indexOf(previousTarget) + activeIds.length) % activeIds.length),
|
|
1270
|
+
)
|
|
1271
|
+
: tiedWeakest;
|
|
1272
|
+
const targetComponent = rotatedCandidates[0] ?? null;
|
|
1273
|
+
const targetDimension =
|
|
1274
|
+
targetComponent === null
|
|
1275
|
+
? null
|
|
1276
|
+
: dimensions.reduce((weakest, dimension) =>
|
|
1277
|
+
componentScores[targetComponent][dimension] < componentScores[targetComponent][weakest]
|
|
1278
|
+
? dimension
|
|
1279
|
+
: weakest,
|
|
1280
|
+
);
|
|
1281
|
+
if (
|
|
1282
|
+
result.targeting &&
|
|
1283
|
+
((result.targeting.target_component_id !== undefined &&
|
|
1284
|
+
result.targeting.target_component_id !== targetComponent) ||
|
|
1285
|
+
(result.targeting.weakest_component_id !== undefined &&
|
|
1286
|
+
result.targeting.weakest_component_id !== targetComponent) ||
|
|
1287
|
+
(result.targeting.target_dimension !== undefined && result.targeting.target_dimension !== targetDimension) ||
|
|
1288
|
+
(result.targeting.weakest_dimension !== undefined && result.targeting.weakest_dimension !== targetDimension))
|
|
1289
|
+
)
|
|
1290
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1291
|
+
const weightedUnits = weightedAmbiguityUnits(result.global_scores, type);
|
|
1292
|
+
const weightedAmbiguity = weightedUnits / 10_000;
|
|
1293
|
+
const resolution = result.bookkeeping?.resolution ?? (result.auto_answered ? "auto_answer" : "direct");
|
|
1294
|
+
const priorStreak = typeof state.auto_answer_streak === "number" ? state.auto_answer_streak : 0;
|
|
1295
|
+
const autoAnswerStreak =
|
|
1296
|
+
resolution === "auto_answer" || resolution === "auto_research_accepted" ? priorStreak + 1 : 0;
|
|
1297
|
+
const requestedRoundIds = result.bookkeeping?.round_ids ?? [roundKey];
|
|
1298
|
+
const durableRoundReferences = new Set(
|
|
1299
|
+
rounds.flatMap(round =>
|
|
1300
|
+
typeof round.round_id === "string" && round.round_id !== ""
|
|
1301
|
+
? [round.round_key, round.round_id]
|
|
1302
|
+
: [round.round_key],
|
|
1303
|
+
),
|
|
1304
|
+
);
|
|
1305
|
+
if (
|
|
1306
|
+
new Set(requestedRoundIds).size !== requestedRoundIds.length ||
|
|
1307
|
+
!requestedRoundIds.every(id => typeof id === "string" && durableRoundReferences.has(id))
|
|
1308
|
+
)
|
|
1309
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1310
|
+
const appendRoundIds = (field: string): string[] => [
|
|
1311
|
+
...(Array.isArray(state[field]) ? state[field].filter((id): id is string => typeof id === "string") : []),
|
|
1312
|
+
...requestedRoundIds.filter(id => !(Array.isArray(state[field]) ? state[field] : []).includes(id)),
|
|
1313
|
+
];
|
|
1314
|
+
const counters = { ...(isPlainObject(state.counters) ? state.counters : {}) };
|
|
1315
|
+
for (const [key, delta] of Object.entries(result.bookkeeping?.counter_deltas ?? {})) {
|
|
1316
|
+
if (
|
|
1317
|
+
!Number.isSafeInteger(delta) ||
|
|
1318
|
+
(typeof counters[key] !== "undefined" && !Number.isSafeInteger(counters[key]))
|
|
1319
|
+
)
|
|
1320
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1321
|
+
counters[key] = (typeof counters[key] === "number" ? counters[key] : 0) + delta;
|
|
1322
|
+
}
|
|
1323
|
+
const nextState: Record<string, unknown> = {
|
|
1324
|
+
...state,
|
|
1325
|
+
topology:
|
|
1326
|
+
isPlainObject(nextTopology) && targetComponent !== null
|
|
1327
|
+
? { ...nextTopology, last_targeted_component_id: targetComponent }
|
|
1328
|
+
: nextTopology,
|
|
1329
|
+
established_facts: facts,
|
|
1330
|
+
ontology_snapshots: ontologySnapshots,
|
|
1331
|
+
auto_answer_streak: autoAnswerStreak,
|
|
1332
|
+
counters,
|
|
1333
|
+
auto_answered_rounds:
|
|
1334
|
+
resolution === "auto_answer" ? appendRoundIds("auto_answered_rounds") : state.auto_answered_rounds,
|
|
1335
|
+
auto_research_accepted_rounds:
|
|
1336
|
+
resolution === "auto_research_accepted"
|
|
1337
|
+
? appendRoundIds("auto_research_accepted_rounds")
|
|
1338
|
+
: state.auto_research_accepted_rounds,
|
|
1339
|
+
refined_rounds: resolution === "refined" ? appendRoundIds("refined_rounds") : state.refined_rounds,
|
|
1340
|
+
};
|
|
1341
|
+
const floorBreakdown = computeAmbiguityFloor({
|
|
1342
|
+
...nextState,
|
|
1343
|
+
rounds: [...rounds, { ...shell, lifecycle: "scored" }],
|
|
1344
|
+
});
|
|
1345
|
+
const floor = floorBreakdown.floor;
|
|
1346
|
+
const effectiveAmbiguity = clampReportedAmbiguity(weightedAmbiguity, floor).effective;
|
|
1347
|
+
const threshold = state.threshold ?? envelope.threshold;
|
|
1348
|
+
if (!finiteScore(threshold)) throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1349
|
+
const thresholdUnits = state.threshold_units ?? envelope.threshold_units ?? scoreToUnits(threshold);
|
|
1350
|
+
if (
|
|
1351
|
+
typeof thresholdUnits !== "number" ||
|
|
1352
|
+
!Number.isSafeInteger(thresholdUnits) ||
|
|
1353
|
+
thresholdUnits < 1 ||
|
|
1354
|
+
thresholdUnits > 10_000 ||
|
|
1355
|
+
scoreToUnits(threshold) !== thresholdUnits
|
|
1356
|
+
)
|
|
1357
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1358
|
+
const milestone = deriveAmbiguityMilestone(scoreToUnits(effectiveAmbiguity), thresholdUnits);
|
|
1359
|
+
nextState.threshold_units = thresholdUnits;
|
|
1360
|
+
const priorScoredRounds = rounds
|
|
1361
|
+
.filter(
|
|
1362
|
+
round =>
|
|
1363
|
+
round.lifecycle === "scored" &&
|
|
1364
|
+
(round.round < shell.round || (round.round === shell.round && round.round_key < shell.round_key)),
|
|
1365
|
+
)
|
|
1366
|
+
.sort((a, b) => b.round - a.round || b.round_key.localeCompare(a.round_key));
|
|
1367
|
+
const priorRound = priorScoredRounds[0];
|
|
1368
|
+
const priorEffectiveAmbiguity = typeof priorRound?.ambiguity === "number" ? priorRound.ambiguity : null;
|
|
1369
|
+
for (const trigger of result.triggers ?? []) {
|
|
1370
|
+
if (trigger.status !== "active") continue;
|
|
1371
|
+
const topologyComponents: unknown[] =
|
|
1372
|
+
isPlainObject(topology) && Array.isArray(topology.components) ? topology.components : [];
|
|
1373
|
+
const hasTopologyComponents = topologyComponents.length > 0;
|
|
1374
|
+
const priorComponent = topologyComponents.find(
|
|
1375
|
+
component => isPlainObject(component) && component.id === trigger.component,
|
|
1376
|
+
);
|
|
1377
|
+
const priorScores = isPlainObject(priorComponent) ? priorComponent.clarity_scores : undefined;
|
|
1378
|
+
const priorDimension = hasTopologyComponents
|
|
1379
|
+
? isPlainObject(priorScores)
|
|
1380
|
+
? priorScores[trigger.dimension]
|
|
1381
|
+
: undefined
|
|
1382
|
+
: priorRound?.scores?.[trigger.dimension];
|
|
1383
|
+
const candidateDimension = hasTopologyComponents
|
|
1384
|
+
? componentScores[trigger.component]?.[trigger.dimension as DeepInterviewDimension]
|
|
1385
|
+
: result.global_scores[trigger.dimension as DeepInterviewDimension];
|
|
1386
|
+
if (
|
|
1387
|
+
!priorRound ||
|
|
1388
|
+
!finiteScore(priorDimension) ||
|
|
1389
|
+
!finiteScore(candidateDimension) ||
|
|
1390
|
+
!finiteScore(priorRound.ambiguity) ||
|
|
1391
|
+
candidateDimension > priorDimension ||
|
|
1392
|
+
effectiveAmbiguity <= priorRound.ambiguity
|
|
1393
|
+
)
|
|
1394
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1395
|
+
}
|
|
1396
|
+
rounds[index] = {
|
|
1397
|
+
...shell,
|
|
1398
|
+
lifecycle: "scored",
|
|
1399
|
+
scored_at: now,
|
|
1400
|
+
scores: result.global_scores,
|
|
1401
|
+
ambiguity: effectiveAmbiguity,
|
|
1402
|
+
reported_ambiguity: weightedAmbiguity,
|
|
1403
|
+
ambiguity_floor: floor,
|
|
1404
|
+
triggers: result.triggers ?? [],
|
|
1405
|
+
round_result_digest: digest,
|
|
1406
|
+
};
|
|
1407
|
+
nextState.rounds = rounds;
|
|
1408
|
+
nextState.current_ambiguity = effectiveAmbiguity;
|
|
1409
|
+
nextState.weighted_ambiguity = weightedAmbiguity;
|
|
1410
|
+
nextState.effective_ambiguity = effectiveAmbiguity;
|
|
1411
|
+
nextState.floor = floor;
|
|
1412
|
+
nextState.ambiguity_milestone = milestone;
|
|
1413
|
+
return {
|
|
1414
|
+
kind: "write",
|
|
1415
|
+
envelope: { ...envelope, state: nextState },
|
|
1416
|
+
projection: {
|
|
1417
|
+
score_units: Object.fromEntries(
|
|
1418
|
+
dimensions.map(dimension => [dimension, scoreToUnits(result.global_scores[dimension])]),
|
|
1419
|
+
) as Partial<Record<DeepInterviewDimension, number>>,
|
|
1420
|
+
weighted_ambiguity: weightedAmbiguity,
|
|
1421
|
+
weighted_ambiguity_units: weightedUnits,
|
|
1422
|
+
floor,
|
|
1423
|
+
floor_units: scoreToUnits(floor),
|
|
1424
|
+
floor_cause: floorBreakdown,
|
|
1425
|
+
effective_ambiguity: effectiveAmbiguity,
|
|
1426
|
+
effective_ambiguity_units: scoreToUnits(effectiveAmbiguity),
|
|
1427
|
+
prior_effective_ambiguity: priorEffectiveAmbiguity,
|
|
1428
|
+
direction:
|
|
1429
|
+
priorEffectiveAmbiguity === null
|
|
1430
|
+
? "initial"
|
|
1431
|
+
: effectiveAmbiguity > priorEffectiveAmbiguity
|
|
1432
|
+
? "increased"
|
|
1433
|
+
: effectiveAmbiguity < priorEffectiveAmbiguity
|
|
1434
|
+
? "decreased"
|
|
1435
|
+
: "unchanged",
|
|
1436
|
+
ambiguity_milestone: milestone,
|
|
1437
|
+
topology: nextState.topology,
|
|
1438
|
+
topology_counts: {
|
|
1439
|
+
active: activeComponents.length,
|
|
1440
|
+
deferred: Array.isArray(topology?.components)
|
|
1441
|
+
? topology.components.filter(component => isPlainObject(component) && component.status === "deferred")
|
|
1442
|
+
.length
|
|
1443
|
+
: 0,
|
|
1444
|
+
total: Array.isArray(topology?.components) ? topology.components.length : 0,
|
|
1445
|
+
},
|
|
1446
|
+
ontology: ontologySnapshots.at(-1),
|
|
1447
|
+
ontology_counts: { stable, changed, new: fresh, basis },
|
|
1448
|
+
targeting: {
|
|
1449
|
+
target_component_id: targetComponent,
|
|
1450
|
+
target_dimension: targetDimension,
|
|
1451
|
+
last_targeted_component_id: targetComponent,
|
|
1452
|
+
},
|
|
1453
|
+
transition: { round_key: roundKey, lifecycle: "scored", auto_answer_streak: autoAnswerStreak },
|
|
1454
|
+
},
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
export function validateDeepInterviewV1Envelope(value: Record<string, unknown>): void {
|
|
1458
|
+
const invalid = (): never => {
|
|
1459
|
+
throw new Error("DI_STATE_SCHEMA_INVALID");
|
|
1460
|
+
};
|
|
1461
|
+
const validDate = (candidate: unknown): candidate is string =>
|
|
1462
|
+
typeof candidate === "string" && Number.isFinite(Date.parse(candidate));
|
|
1463
|
+
const validScore = (candidate: unknown): candidate is number => {
|
|
1464
|
+
if (typeof candidate !== "number") return false;
|
|
1465
|
+
try {
|
|
1466
|
+
scoreToUnits(candidate);
|
|
1467
|
+
return true;
|
|
1468
|
+
} catch {
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
const stateValue = value.state;
|
|
1473
|
+
if (value.skill !== "deep-interview" || value.schema_version !== 1 || !isPlainObject(stateValue)) return invalid();
|
|
1474
|
+
const state = stateValue as Record<string, unknown>;
|
|
1475
|
+
const type = state.type;
|
|
1476
|
+
if (type !== "greenfield" && type !== "brownfield") return invalid();
|
|
1477
|
+
const dimensions: DeepInterviewDimension[] =
|
|
1478
|
+
type === "brownfield" ? ["goal", "constraints", "criteria", "context"] : ["goal", "constraints", "criteria"];
|
|
1479
|
+
const isDimension = (candidate: unknown): candidate is DeepInterviewDimension =>
|
|
1480
|
+
typeof candidate === "string" && dimensions.includes(candidate as DeepInterviewDimension);
|
|
1481
|
+
const roundsValue = state.rounds;
|
|
1482
|
+
const factsValue = state.established_facts;
|
|
1483
|
+
const threshold = state.threshold ?? value.threshold;
|
|
1484
|
+
const thresholdUnits = state.threshold_units;
|
|
1485
|
+
if (
|
|
1486
|
+
!validScore(threshold) ||
|
|
1487
|
+
typeof thresholdUnits !== "number" ||
|
|
1488
|
+
!Number.isSafeInteger(thresholdUnits) ||
|
|
1489
|
+
thresholdUnits < 1 ||
|
|
1490
|
+
thresholdUnits > 10_000 ||
|
|
1491
|
+
scoreToUnits(threshold) !== thresholdUnits ||
|
|
1492
|
+
!Array.isArray(roundsValue) ||
|
|
1493
|
+
!Array.isArray(factsValue)
|
|
1494
|
+
)
|
|
1495
|
+
return invalid();
|
|
1496
|
+
const rounds = roundsValue as unknown[];
|
|
1497
|
+
const facts = factsValue as unknown[];
|
|
1498
|
+
|
|
1499
|
+
const topology = state.topology;
|
|
1500
|
+
const componentIds = new Set<string>();
|
|
1501
|
+
let hasTopologyComponents = false;
|
|
1502
|
+
if (topology !== undefined) {
|
|
1503
|
+
if (!isPlainObject(topology) || !Array.isArray(topology.components) || typeof topology.status !== "string")
|
|
1504
|
+
return invalid();
|
|
1505
|
+
const topologyRecord = topology as Record<string, unknown>;
|
|
1506
|
+
const components = topologyRecord.components as unknown[];
|
|
1507
|
+
for (const component of components) {
|
|
1508
|
+
if (!isPlainObject(component)) return invalid();
|
|
1509
|
+
const componentRecord = component as Record<string, unknown>;
|
|
1510
|
+
const componentIdValue = componentRecord.id;
|
|
1511
|
+
if (typeof componentIdValue !== "string" || componentIdValue === "" || componentIds.has(componentIdValue))
|
|
1512
|
+
return invalid();
|
|
1513
|
+
const componentId = componentIdValue;
|
|
1514
|
+
componentIds.add(componentId);
|
|
1515
|
+
if (componentRecord.active !== undefined && typeof componentRecord.active !== "boolean") return invalid();
|
|
1516
|
+
if (componentRecord.clarity_scores !== undefined) {
|
|
1517
|
+
const clarityScoresValue = componentRecord.clarity_scores;
|
|
1518
|
+
if (!isPlainObject(clarityScoresValue)) return invalid();
|
|
1519
|
+
const clarityScores = clarityScoresValue as Record<string, unknown>;
|
|
1520
|
+
for (const dimension of dimensions) {
|
|
1521
|
+
const score = clarityScores[dimension];
|
|
1522
|
+
if (score !== undefined && score !== null && !validScore(score)) return invalid();
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
hasTopologyComponents = componentIds.size > 0;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const factIds = new Set<string>();
|
|
1530
|
+
for (const fact of facts) {
|
|
1531
|
+
if (!isPlainObject(fact)) return invalid();
|
|
1532
|
+
const factRecord = fact as Record<string, unknown>;
|
|
1533
|
+
const factIdValue = factRecord.id;
|
|
1534
|
+
const factRoundValue = factRecord.round;
|
|
1535
|
+
if (
|
|
1536
|
+
typeof factIdValue !== "string" ||
|
|
1537
|
+
factIdValue === "" ||
|
|
1538
|
+
factIds.has(factIdValue) ||
|
|
1539
|
+
typeof factRecord.statement !== "string" ||
|
|
1540
|
+
factRecord.statement === "" ||
|
|
1541
|
+
typeof factRoundValue !== "number" ||
|
|
1542
|
+
!Number.isSafeInteger(factRoundValue) ||
|
|
1543
|
+
factRoundValue < 1 ||
|
|
1544
|
+
typeof factRecord.disputed !== "boolean"
|
|
1545
|
+
)
|
|
1546
|
+
return invalid();
|
|
1547
|
+
const factId = factIdValue;
|
|
1548
|
+
factIds.add(factId);
|
|
1549
|
+
if (
|
|
1550
|
+
factRecord.component !== undefined &&
|
|
1551
|
+
(typeof factRecord.component !== "string" ||
|
|
1552
|
+
factRecord.component === "" ||
|
|
1553
|
+
(hasTopologyComponents && !componentIds.has(factRecord.component)))
|
|
1554
|
+
)
|
|
1555
|
+
return invalid();
|
|
1556
|
+
if (factRecord.dimension !== undefined && !isDimension(factRecord.dimension)) return invalid();
|
|
1557
|
+
}
|
|
1558
|
+
for (const fact of facts) {
|
|
1559
|
+
if (!isPlainObject(fact)) return invalid();
|
|
1560
|
+
const factRecord = fact as Record<string, unknown>;
|
|
1561
|
+
if (
|
|
1562
|
+
factRecord.superseded_by !== undefined &&
|
|
1563
|
+
(factRecord.disputed !== true || !factIds.has(String(factRecord.superseded_by)))
|
|
1564
|
+
)
|
|
1565
|
+
return invalid();
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (state.ontology_snapshots !== undefined) {
|
|
1569
|
+
const ontologySnapshotsValue = state.ontology_snapshots;
|
|
1570
|
+
if (!Array.isArray(ontologySnapshotsValue)) return invalid();
|
|
1571
|
+
const ontologySnapshots = ontologySnapshotsValue as unknown[];
|
|
1572
|
+
let priorRound = 0;
|
|
1573
|
+
for (const snapshot of ontologySnapshots) {
|
|
1574
|
+
if (!isPlainObject(snapshot)) return invalid();
|
|
1575
|
+
const snapshotRecord = snapshot as Record<string, unknown>;
|
|
1576
|
+
const snapshotRound = snapshotRecord.round;
|
|
1577
|
+
const stableEntities = snapshotRecord.stable_entities;
|
|
1578
|
+
const newEntities = snapshotRecord.new_entities;
|
|
1579
|
+
const changedEntities = snapshotRecord.changed_entities;
|
|
1580
|
+
const stabilityRatio = snapshotRecord.stability_ratio;
|
|
1581
|
+
if (
|
|
1582
|
+
typeof snapshotRound !== "number" ||
|
|
1583
|
+
!Number.isSafeInteger(snapshotRound) ||
|
|
1584
|
+
snapshotRound <= priorRound ||
|
|
1585
|
+
!validDate(snapshotRecord.captured_at) ||
|
|
1586
|
+
!Array.isArray(snapshotRecord.entities) ||
|
|
1587
|
+
!["no_entities", "first_round", "compared"].includes(String(snapshotRecord.basis)) ||
|
|
1588
|
+
typeof stableEntities !== "number" ||
|
|
1589
|
+
!Number.isSafeInteger(stableEntities) ||
|
|
1590
|
+
typeof newEntities !== "number" ||
|
|
1591
|
+
!Number.isSafeInteger(newEntities) ||
|
|
1592
|
+
typeof changedEntities !== "number" ||
|
|
1593
|
+
!Number.isSafeInteger(changedEntities) ||
|
|
1594
|
+
(stabilityRatio !== null && !validScore(stabilityRatio))
|
|
1595
|
+
)
|
|
1596
|
+
return invalid();
|
|
1597
|
+
const entities = snapshotRecord.entities as unknown[];
|
|
1598
|
+
if (
|
|
1599
|
+
stableEntities < 0 ||
|
|
1600
|
+
newEntities < 0 ||
|
|
1601
|
+
changedEntities < 0 ||
|
|
1602
|
+
stableEntities + newEntities + changedEntities !== entities.length ||
|
|
1603
|
+
((snapshotRecord.basis === "no_entities" || snapshotRecord.basis === "first_round") &&
|
|
1604
|
+
stabilityRatio !== null) ||
|
|
1605
|
+
(snapshotRecord.basis === "compared" && stabilityRatio === null)
|
|
1606
|
+
)
|
|
1607
|
+
return invalid();
|
|
1608
|
+
priorRound = snapshotRound;
|
|
1609
|
+
for (const entity of entities) {
|
|
1610
|
+
if (!isPlainObject(entity)) return invalid();
|
|
1611
|
+
const entityRecord = entity as Record<string, unknown>;
|
|
1612
|
+
if (
|
|
1613
|
+
typeof entityRecord.id !== "string" ||
|
|
1614
|
+
typeof entityRecord.name !== "string" ||
|
|
1615
|
+
entityRecord.name === "" ||
|
|
1616
|
+
typeof entityRecord.type !== "string" ||
|
|
1617
|
+
!Array.isArray(entityRecord.fields) ||
|
|
1618
|
+
!entityRecord.fields.every((field: unknown) => typeof field === "string") ||
|
|
1619
|
+
!Array.isArray(entityRecord.relationships) ||
|
|
1620
|
+
!entityRecord.relationships.every((relationship: unknown) => typeof relationship === "string")
|
|
1621
|
+
)
|
|
1622
|
+
return invalid();
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
if (state.auto_answered_rounds !== undefined) {
|
|
1627
|
+
const autoAnsweredRoundsValue = state.auto_answered_rounds;
|
|
1628
|
+
if (!Array.isArray(autoAnsweredRoundsValue)) return invalid();
|
|
1629
|
+
const autoAnsweredRounds = autoAnsweredRoundsValue as unknown[];
|
|
1630
|
+
if (!autoAnsweredRounds.every((key: unknown) => typeof key === "string")) return invalid();
|
|
1631
|
+
}
|
|
1632
|
+
for (const key of ["current_ambiguity", "weighted_ambiguity", "effective_ambiguity", "floor"] as const) {
|
|
1633
|
+
if (state[key] !== undefined && !validScore(state[key])) return invalid();
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
for (const round of rounds) {
|
|
1637
|
+
if (!isPlainObject(round)) return invalid();
|
|
1638
|
+
const roundRecord = round as Record<string, unknown>;
|
|
1639
|
+
const roundNumberValue = roundRecord.round;
|
|
1640
|
+
const isRoundZeroIntentShell =
|
|
1641
|
+
roundNumberValue === 0 && roundRecord.lifecycle === "answered" && roundRecord.scores === undefined;
|
|
1642
|
+
if (
|
|
1643
|
+
typeof roundRecord.round_key !== "string" ||
|
|
1644
|
+
roundRecord.round_key === "" ||
|
|
1645
|
+
typeof roundNumberValue !== "number" ||
|
|
1646
|
+
!Number.isSafeInteger(roundNumberValue) ||
|
|
1647
|
+
roundNumberValue < 0 ||
|
|
1648
|
+
(roundNumberValue === 0 && !isRoundZeroIntentShell) ||
|
|
1649
|
+
typeof roundRecord.question_id !== "string" ||
|
|
1650
|
+
roundRecord.question_id === "" ||
|
|
1651
|
+
typeof roundRecord.question_text !== "string" ||
|
|
1652
|
+
typeof roundRecord.question_hash !== "string" ||
|
|
1653
|
+
roundRecord.question_hash === "" ||
|
|
1654
|
+
typeof roundRecord.answer_hash !== "string" ||
|
|
1655
|
+
roundRecord.answer_hash === "" ||
|
|
1656
|
+
!validDate(roundRecord.answered_at) ||
|
|
1657
|
+
!["answered", "pending_scoring", "scored"].includes(String(roundRecord.lifecycle))
|
|
1658
|
+
)
|
|
1659
|
+
return invalid();
|
|
1660
|
+
if (
|
|
1661
|
+
roundRecord.component !== undefined &&
|
|
1662
|
+
(typeof roundRecord.component !== "string" ||
|
|
1663
|
+
roundRecord.component === "" ||
|
|
1664
|
+
(hasTopologyComponents &&
|
|
1665
|
+
!componentIds.has(roundRecord.component) &&
|
|
1666
|
+
!(isRoundZeroIntentShell && roundRecord.component === "review-topology")))
|
|
1667
|
+
)
|
|
1668
|
+
return invalid();
|
|
1669
|
+
if (
|
|
1670
|
+
roundRecord.dimension !== undefined &&
|
|
1671
|
+
!isDimension(roundRecord.dimension) &&
|
|
1672
|
+
!(isRoundZeroIntentShell && roundRecord.dimension === "topology")
|
|
1673
|
+
)
|
|
1674
|
+
return invalid();
|
|
1675
|
+
if (roundRecord.lifecycle !== "scored") continue;
|
|
1676
|
+
const scoresValue = roundRecord.scores;
|
|
1677
|
+
const triggersValue = roundRecord.triggers;
|
|
1678
|
+
if (
|
|
1679
|
+
!validDate(roundRecord.scored_at) ||
|
|
1680
|
+
!isPlainObject(scoresValue) ||
|
|
1681
|
+
!validScore(roundRecord.ambiguity) ||
|
|
1682
|
+
!validScore(roundRecord.reported_ambiguity) ||
|
|
1683
|
+
!validScore(roundRecord.ambiguity_floor) ||
|
|
1684
|
+
typeof roundRecord.round_result_digest !== "string" ||
|
|
1685
|
+
!/^[0-9a-f]{64}$/.test(roundRecord.round_result_digest) ||
|
|
1686
|
+
!Array.isArray(triggersValue)
|
|
1687
|
+
)
|
|
1688
|
+
return invalid();
|
|
1689
|
+
const scores = scoresValue as Record<string, unknown>;
|
|
1690
|
+
if (!dimensions.every(dimension => validScore(scores[dimension]))) return invalid();
|
|
1691
|
+
const triggers = triggersValue as unknown[];
|
|
1692
|
+
for (const trigger of triggers) {
|
|
1693
|
+
if (!isPlainObject(trigger)) return invalid();
|
|
1694
|
+
const triggerRecord = trigger as Record<string, unknown>;
|
|
1695
|
+
if (
|
|
1696
|
+
!["A", "B", "C", "D"].includes(String(triggerRecord.kind)) ||
|
|
1697
|
+
!["active", "disputed", "unresolved"].includes(String(triggerRecord.status)) ||
|
|
1698
|
+
typeof triggerRecord.name !== "string" ||
|
|
1699
|
+
typeof triggerRecord.component !== "string" ||
|
|
1700
|
+
triggerRecord.component === "" ||
|
|
1701
|
+
(hasTopologyComponents && !componentIds.has(triggerRecord.component)) ||
|
|
1702
|
+
!isDimension(triggerRecord.dimension) ||
|
|
1703
|
+
(triggerRecord.contradictedFactId !== undefined && !factIds.has(String(triggerRecord.contradictedFactId)))
|
|
1704
|
+
)
|
|
1705
|
+
return invalid();
|
|
1706
|
+
if (
|
|
1707
|
+
(triggerRecord.status === "disputed" || triggerRecord.status === "unresolved") &&
|
|
1708
|
+
typeof triggerRecord.rationale !== "string"
|
|
1709
|
+
)
|
|
1710
|
+
return invalid();
|
|
1711
|
+
}
|
|
1712
|
+
void scores;
|
|
1713
|
+
}
|
|
1714
|
+
}
|