@lucern/graph-primitives 1.0.0 → 1.0.1
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/README.md +13 -12
- package/dist/beliefDecay.js +24 -17
- package/dist/beliefDecay.js.map +1 -1
- package/dist/beliefEvidenceLinks.js +32 -8
- package/dist/beliefEvidenceLinks.js.map +1 -1
- package/dist/confidencePropagationDispatch.js.map +1 -1
- package/dist/contradictions.js +32 -9
- package/dist/contradictions.js.map +1 -1
- package/dist/convex.d.ts +55 -12
- package/dist/convex.js.map +1 -1
- package/dist/edgeValidation.d.ts +25 -2
- package/dist/edges/index.d.ts +9 -2
- package/dist/edges/index.js.map +1 -1
- package/dist/edges/propagationTypes.d.ts +2 -3
- package/dist/edges/propagationTypes.js.map +1 -1
- package/dist/entityBridge.js +10 -3
- package/dist/entityBridge.js.map +1 -1
- package/dist/entityLifecycle.js +15 -3
- package/dist/entityLifecycle.js.map +1 -1
- package/dist/epistemicAnswers.js.map +1 -1
- package/dist/epistemicBeliefs.admin.d.ts +36 -0
- package/dist/epistemicBeliefs.admin.js +745 -0
- package/dist/epistemicBeliefs.admin.js.map +1 -0
- package/dist/epistemicBeliefs.backfills.d.ts +62 -0
- package/dist/epistemicBeliefs.backfills.js +1004 -0
- package/dist/epistemicBeliefs.backfills.js.map +1 -0
- package/dist/epistemicBeliefs.confidence.d.ts +45 -0
- package/dist/epistemicBeliefs.confidence.js +1285 -0
- package/dist/epistemicBeliefs.confidence.js.map +1 -0
- package/dist/epistemicBeliefs.core.d.ts +35 -0
- package/dist/epistemicBeliefs.core.js +1508 -0
- package/dist/epistemicBeliefs.core.js.map +1 -0
- package/dist/epistemicBeliefs.d.ts +12 -3
- package/dist/epistemicBeliefs.helpers.d.ts +168 -0
- package/dist/epistemicBeliefs.helpers.js +1060 -0
- package/dist/epistemicBeliefs.helpers.js.map +1 -0
- package/dist/epistemicBeliefs.internal.d.ts +30 -0
- package/dist/epistemicBeliefs.internal.js +1329 -0
- package/dist/epistemicBeliefs.internal.js.map +1 -0
- package/dist/epistemicBeliefs.js +1196 -1184
- package/dist/epistemicBeliefs.js.map +1 -1
- package/dist/epistemicBeliefs.lifecycle.d.ts +19 -0
- package/dist/epistemicBeliefs.lifecycle.js +1608 -0
- package/dist/epistemicBeliefs.lifecycle.js.map +1 -0
- package/dist/epistemicBeliefs.links.d.ts +30 -0
- package/dist/epistemicBeliefs.links.js +761 -0
- package/dist/epistemicBeliefs.links.js.map +1 -0
- package/dist/epistemicBeliefs.queries.d.ts +16 -0
- package/dist/epistemicBeliefs.queries.js +90 -0
- package/dist/epistemicBeliefs.queries.js.map +1 -0
- package/dist/epistemicContractHelpers.d.ts +1 -1
- package/dist/epistemicContractHelpers.js +1 -1
- package/dist/epistemicContracts.d.ts +5 -76
- package/dist/epistemicContracts.evaluators.d.ts +36 -0
- package/dist/epistemicContracts.evaluators.js +2506 -0
- package/dist/epistemicContracts.evaluators.js.map +1 -0
- package/dist/epistemicContracts.handlers.d.ts +40 -0
- package/dist/epistemicContracts.handlers.js +3029 -0
- package/dist/epistemicContracts.handlers.js.map +1 -0
- package/dist/epistemicContracts.js +2006 -5281
- package/dist/epistemicContracts.js.map +1 -1
- package/dist/epistemicContracts.metrics.d.ts +26 -0
- package/dist/epistemicContracts.metrics.js +427 -0
- package/dist/epistemicContracts.metrics.js.map +1 -0
- package/dist/epistemicContracts.types.d.ts +159 -0
- package/dist/epistemicContracts.types.js +3 -0
- package/dist/epistemicContracts.types.js.map +1 -0
- package/dist/epistemicEdgeCreation.d.ts +73 -0
- package/dist/epistemicEdgeCreation.js +450 -0
- package/dist/epistemicEdgeCreation.js.map +1 -0
- package/dist/epistemicEdges-BF-cn4i3.d.ts +43 -0
- package/dist/epistemicEdges.d.ts +8 -1
- package/dist/epistemicEdges.handlers.d.ts +20 -0
- package/dist/epistemicEdges.handlers.js +289 -0
- package/dist/epistemicEdges.handlers.js.map +1 -0
- package/dist/epistemicEdges.helpers.d.ts +27 -0
- package/dist/epistemicEdges.helpers.js +162 -0
- package/dist/epistemicEdges.helpers.js.map +1 -0
- package/dist/epistemicEdges.js +797 -875
- package/dist/epistemicEdges.js.map +1 -1
- package/dist/epistemicEdges.mutations.d.ts +39 -0
- package/dist/epistemicEdges.mutations.js +1365 -0
- package/dist/epistemicEdges.mutations.js.map +1 -0
- package/dist/epistemicEdges.queries.d.ts +95 -0
- package/dist/epistemicEdges.queries.js +851 -0
- package/dist/epistemicEdges.queries.js.map +1 -0
- package/dist/epistemicEdges.types.d.ts +32 -0
- package/dist/epistemicEdges.types.js +3 -0
- package/dist/epistemicEdges.types.js.map +1 -0
- package/dist/epistemicEvidence-DvfchNt7.d.ts +46 -0
- package/dist/epistemicEvidence.d.ts +5 -2
- package/dist/epistemicEvidence.js +801 -807
- package/dist/epistemicEvidence.js.map +1 -1
- package/dist/epistemicEvidenceHelpers.d.ts +71 -0
- package/dist/epistemicEvidenceHelpers.js +769 -0
- package/dist/epistemicEvidenceHelpers.js.map +1 -0
- package/dist/epistemicEvidenceMutations.d.ts +10 -0
- package/dist/epistemicEvidenceMutations.js +1421 -0
- package/dist/epistemicEvidenceMutations.js.map +1 -0
- package/dist/epistemicEvidenceQueries.d.ts +10 -0
- package/dist/epistemicEvidenceQueries.js +1049 -0
- package/dist/epistemicEvidenceQueries.js.map +1 -0
- package/dist/epistemicHelpers.d.ts +4 -2
- package/dist/epistemicHelpers.js +132 -127
- package/dist/epistemicHelpers.js.map +1 -1
- package/dist/epistemicLayerRules.d.ts +138 -0
- package/dist/epistemicLayerRules.js +481 -0
- package/dist/epistemicLayerRules.js.map +1 -0
- package/dist/epistemicLinking.js +1 -1
- package/dist/epistemicLinking.js.map +1 -1
- package/dist/epistemicNodeCreation.d.ts +101 -0
- package/dist/epistemicNodeCreation.js +709 -0
- package/dist/epistemicNodeCreation.js.map +1 -0
- package/dist/epistemicNodes-BCQxpYx_.d.ts +54 -0
- package/dist/epistemicNodes.d.ts +5 -1
- package/dist/epistemicNodes.helpers.d.ts +51 -0
- package/dist/epistemicNodes.helpers.js +73 -0
- package/dist/epistemicNodes.helpers.js.map +1 -0
- package/dist/epistemicNodes.internal.d.ts +34 -0
- package/dist/epistemicNodes.internal.js +658 -0
- package/dist/epistemicNodes.internal.js.map +1 -0
- package/dist/epistemicNodes.js +698 -693
- package/dist/epistemicNodes.js.map +1 -1
- package/dist/epistemicNodes.mutations.d.ts +34 -0
- package/dist/epistemicNodes.mutations.js +1153 -0
- package/dist/epistemicNodes.mutations.js.map +1 -0
- package/dist/epistemicNodes.queries.d.ts +36 -0
- package/dist/epistemicNodes.queries.js +619 -0
- package/dist/epistemicNodes.queries.js.map +1 -0
- package/dist/epistemicNodes.validators.d.ts +23 -0
- package/dist/epistemicNodes.validators.js +105 -0
- package/dist/epistemicNodes.validators.js.map +1 -0
- package/dist/epistemicQuestions-bwHd2FWE.d.ts +68 -0
- package/dist/epistemicQuestions.conviction.d.ts +52 -0
- package/dist/epistemicQuestions.conviction.js +1389 -0
- package/dist/epistemicQuestions.conviction.js.map +1 -0
- package/dist/epistemicQuestions.create.d.ts +29 -0
- package/dist/epistemicQuestions.create.js +1300 -0
- package/dist/epistemicQuestions.create.js.map +1 -0
- package/dist/epistemicQuestions.d.ts +10 -2
- package/dist/epistemicQuestions.evidence.d.ts +22 -0
- package/dist/epistemicQuestions.evidence.js +929 -0
- package/dist/epistemicQuestions.evidence.js.map +1 -0
- package/dist/epistemicQuestions.helpers.d.ts +69 -0
- package/dist/epistemicQuestions.helpers.js +824 -0
- package/dist/epistemicQuestions.helpers.js.map +1 -0
- package/dist/epistemicQuestions.js +2435 -2430
- package/dist/epistemicQuestions.js.map +1 -1
- package/dist/epistemicQuestions.lifecycle.d.ts +24 -0
- package/dist/epistemicQuestions.lifecycle.js +838 -0
- package/dist/epistemicQuestions.lifecycle.js.map +1 -0
- package/dist/epistemicQuestions.queries.d.ts +41 -0
- package/dist/epistemicQuestions.queries.js +1013 -0
- package/dist/epistemicQuestions.queries.js.map +1 -0
- package/dist/epistemicQuestions.sprint.d.ts +22 -0
- package/dist/epistemicQuestions.sprint.js +757 -0
- package/dist/epistemicQuestions.sprint.js.map +1 -0
- package/dist/epistemicQuestions.tail.d.ts +42 -0
- package/dist/epistemicQuestions.tail.js +1345 -0
- package/dist/epistemicQuestions.tail.js.map +1 -0
- package/dist/epistemicSources.js +6 -2
- package/dist/epistemicSources.js.map +1 -1
- package/dist/evaluators/index.d.ts +2 -2
- package/dist/evaluators/index.js +45 -5320
- package/dist/evaluators/index.js.map +1 -1
- package/dist/evaluators/lintCheckerEvaluator.d.ts +1 -1
- package/dist/evaluators/sentryCheckerEvaluator.d.ts +1 -1
- package/dist/evaluators/testRunnerEvaluator.d.ts +1 -1
- package/dist/evaluators/tscCheckerEvaluator.d.ts +1 -1
- package/dist/{graphTypes-CpgIuCdo.d.ts → graphTypes-B8VaIjnl.d.ts} +1 -1
- package/dist/graphTypes.d.ts +1 -1
- package/dist/{helpers-BYHIk5vU.d.ts → helpers-DNYfg6mo.d.ts} +2 -3
- package/dist/helpers.d.ts +2 -2
- package/dist/helpers.js.map +1 -1
- package/dist/{index-Dq-7R-gi.d.ts → index-C-Kyd7hD.d.ts} +1 -1
- package/dist/index.d.ts +160 -14
- package/dist/index.js +12291 -13001
- package/dist/index.js.map +1 -1
- package/dist/logicalRoleInference.js.map +1 -1
- package/dist/ontologyApproval.js +1 -1
- package/dist/ontologyApproval.js.map +1 -1
- package/dist/ontologyDefinitions.js +25 -7
- package/dist/ontologyDefinitions.js.map +1 -1
- package/dist/ontologyRegistry.js.map +1 -1
- package/dist/projectionReconciliation.js.map +1 -1
- package/dist/questionEvidenceLinks.js +28 -7
- package/dist/questionEvidenceLinks.js.map +1 -1
- package/dist/resolvers.js.map +1 -1
- package/dist/scopeResolverCompat.js.map +1 -1
- package/dist/topicProjectOverlay.js.map +1 -1
- package/dist/topicScope.js.map +1 -1
- package/dist/workflowBridge.js.map +1 -1
- package/dist/workspaceIsolation.js.map +1 -1
- package/package.json +4 -5
- package/dist/edgeValidation-CeI0wc0r.d.ts +0 -35
- package/dist/epistemicBeliefs-DzKjZAeC.d.ts +0 -377
- package/dist/epistemicEdges-CvlKnEyy.d.ts +0 -191
- package/dist/epistemicEvidence-xw6UUrwh.d.ts +0 -128
- package/dist/epistemicHelpers-DevrYgPN.d.ts +0 -329
- package/dist/epistemicNodes-DjSUfvyD.d.ts +0 -167
- package/dist/epistemicQuestions-B_nUclrH.d.ts +0 -214
- package/dist/index-Dct1T70K.d.ts +0 -25
|
@@ -0,0 +1,1389 @@
|
|
|
1
|
+
import { v } from 'convex/values';
|
|
2
|
+
import { checkScopeAccess } from '@lucern/access-control/access';
|
|
3
|
+
import { permissiveReturn } from '@lucern/contracts/schema-helpers/validators';
|
|
4
|
+
import { componentsGeneric, anyApi, mutationGeneric, queryGeneric } from 'convex/server';
|
|
5
|
+
|
|
6
|
+
// src/epistemicQuestions.conviction.ts
|
|
7
|
+
var api = anyApi;
|
|
8
|
+
componentsGeneric();
|
|
9
|
+
var internal = anyApi;
|
|
10
|
+
var mutation = mutationGeneric;
|
|
11
|
+
var query = queryGeneric;
|
|
12
|
+
|
|
13
|
+
// src/debug.ts
|
|
14
|
+
function isGraphPrimitiveDebugEnabled() {
|
|
15
|
+
const env = globalThis.process?.env;
|
|
16
|
+
return env?.LUCERN_COMPAT_FALLBACK_DEBUG === "1" || env?.LUCERN_GRAPH_DEBUG === "1";
|
|
17
|
+
}
|
|
18
|
+
function formatGraphPrimitiveError(error) {
|
|
19
|
+
if (error instanceof Error) {
|
|
20
|
+
return `${error.name}: ${error.message}`;
|
|
21
|
+
}
|
|
22
|
+
if (typeof error === "string") {
|
|
23
|
+
return error;
|
|
24
|
+
}
|
|
25
|
+
if (error === null) {
|
|
26
|
+
return "null";
|
|
27
|
+
}
|
|
28
|
+
if (error === void 0) {
|
|
29
|
+
return "undefined";
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return JSON.stringify(error);
|
|
33
|
+
} catch {
|
|
34
|
+
return Object.prototype.toString.call(error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function debugGraphPrimitiveFallback(message, context) {
|
|
38
|
+
if (!isGraphPrimitiveDebugEnabled()) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.debug(message, context ?? {});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/topicProjectOverlay.ts
|
|
45
|
+
var LEGACY_SCOPE_FIELD = "graphScopeProjectId";
|
|
46
|
+
function readNonEmptyString(value) {
|
|
47
|
+
if (typeof value !== "string") {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const normalized = value.trim();
|
|
51
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
52
|
+
}
|
|
53
|
+
function readStringArray(value) {
|
|
54
|
+
if (!Array.isArray(value)) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
return value.map((entry) => readNonEmptyString(entry)).filter((entry) => Boolean(entry));
|
|
58
|
+
}
|
|
59
|
+
function readMetadata(topic) {
|
|
60
|
+
return topic.metadata && typeof topic.metadata === "object" ? topic.metadata : {};
|
|
61
|
+
}
|
|
62
|
+
function readLegacyProjectId(value) {
|
|
63
|
+
if (!value) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
return readNonEmptyString(value[LEGACY_SCOPE_FIELD]);
|
|
67
|
+
}
|
|
68
|
+
function coerceVisibility(value) {
|
|
69
|
+
return value === "private" || value === "team" || value === "firm" || value === "external" || value === "public" ? value : void 0;
|
|
70
|
+
}
|
|
71
|
+
function coerceStatus(value) {
|
|
72
|
+
return value === "active" || value === "archived" || value === "watching" ? value : void 0;
|
|
73
|
+
}
|
|
74
|
+
function mapProjectType(topic, metadata) {
|
|
75
|
+
const explicit = readNonEmptyString(metadata.projectType);
|
|
76
|
+
if (explicit) {
|
|
77
|
+
return explicit;
|
|
78
|
+
}
|
|
79
|
+
if (topic.type === "theme") {
|
|
80
|
+
return "thematic";
|
|
81
|
+
}
|
|
82
|
+
return readNonEmptyString(topic.type) || "general";
|
|
83
|
+
}
|
|
84
|
+
function isProjectLikeTopic(topic) {
|
|
85
|
+
const metadata = readMetadata(topic);
|
|
86
|
+
return topic.type === "theme" || topic.type === "thematic" || topic.type === "deal" || topic.type === "monitoring" || readLegacyProjectId(topic) !== void 0 || readNonEmptyString(metadata.projectType) !== void 0;
|
|
87
|
+
}
|
|
88
|
+
function isMissingLucernChildComponentError(error) {
|
|
89
|
+
const message = getErrorMessage(error);
|
|
90
|
+
return message.includes(
|
|
91
|
+
'Child component ComponentName(Identifier("lucern")) not found'
|
|
92
|
+
) || message.includes("Child component") && message.includes("lucern") && message.includes("not found");
|
|
93
|
+
}
|
|
94
|
+
function getErrorMessage(error) {
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
return error.message;
|
|
97
|
+
}
|
|
98
|
+
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
|
|
99
|
+
return error.message;
|
|
100
|
+
}
|
|
101
|
+
return "unknown error";
|
|
102
|
+
}
|
|
103
|
+
async function resolveTopicDoc(ctx, scopeId) {
|
|
104
|
+
if (ctx?.db && typeof ctx.db.get === "function") {
|
|
105
|
+
try {
|
|
106
|
+
const directTopic = await ctx.db.get(
|
|
107
|
+
scopeId
|
|
108
|
+
);
|
|
109
|
+
if (directTopic) {
|
|
110
|
+
return directTopic;
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
debugGraphPrimitiveFallback(
|
|
114
|
+
"[topicProjectOverlay] Failed to resolve topic by direct ID",
|
|
115
|
+
{
|
|
116
|
+
error,
|
|
117
|
+
scopeId
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (typeof ctx.runQuery !== "function") {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const topic = await ctx.runQuery(api.topics.get, {
|
|
127
|
+
id: String(scopeId)
|
|
128
|
+
});
|
|
129
|
+
if (topic?.name !== void 0 && topic?.type !== void 0) {
|
|
130
|
+
return topic;
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
debugGraphPrimitiveFallback(
|
|
134
|
+
"[topicProjectOverlay] Failed to resolve topic by ID query",
|
|
135
|
+
{
|
|
136
|
+
error,
|
|
137
|
+
scopeId
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const topic = await ctx.runQuery(api.topics.getByLegacyScopeId, {
|
|
143
|
+
projectId: String(scopeId)
|
|
144
|
+
});
|
|
145
|
+
if (topic?.name !== void 0 && topic?.type !== void 0) {
|
|
146
|
+
return topic;
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
debugGraphPrimitiveFallback(
|
|
150
|
+
"[topicProjectOverlay] Failed to resolve topic by legacy scope ID",
|
|
151
|
+
{ error, scopeId }
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function materializeTopicProjectOverlay(topic, idMode = "legacy") {
|
|
157
|
+
const metadata = readMetadata(topic);
|
|
158
|
+
const topicId = String(topic._id);
|
|
159
|
+
const legacyProjectId = readLegacyProjectId(topic) || readLegacyProjectId(metadata) || readNonEmptyString(metadata.legacyProjectId);
|
|
160
|
+
const storageProjectId = legacyProjectId || topicId;
|
|
161
|
+
const outwardId = idMode === "topic" ? topicId : storageProjectId;
|
|
162
|
+
const visibility = coerceVisibility(topic.visibility) || coerceVisibility(metadata.visibility) || "private";
|
|
163
|
+
const status = coerceStatus(topic.status) || coerceStatus(metadata.status) || "active";
|
|
164
|
+
const createdAt = typeof topic.createdAt === "number" ? topic.createdAt : typeof topic._creationTime === "number" ? topic._creationTime : 0;
|
|
165
|
+
const updatedAt = typeof topic.updatedAt === "number" ? topic.updatedAt : typeof metadata.updatedAt === "number" ? metadata.updatedAt : createdAt;
|
|
166
|
+
return {
|
|
167
|
+
...metadata,
|
|
168
|
+
_id: outwardId,
|
|
169
|
+
projectId: outwardId,
|
|
170
|
+
topicId,
|
|
171
|
+
storageProjectId,
|
|
172
|
+
legacyProjectId,
|
|
173
|
+
name: readNonEmptyString(topic.name) || "Untitled Theme",
|
|
174
|
+
type: mapProjectType(topic, metadata),
|
|
175
|
+
description: readNonEmptyString(topic.description),
|
|
176
|
+
ownerId: readNonEmptyString(metadata.ownerId) || readNonEmptyString(topic.createdBy) || "system",
|
|
177
|
+
sharedWith: readStringArray(metadata.sharedWith),
|
|
178
|
+
visibility,
|
|
179
|
+
tenantId: readNonEmptyString(topic.tenantId) || readNonEmptyString(metadata.tenantId),
|
|
180
|
+
workspaceId: readNonEmptyString(topic.workspaceId) || readNonEmptyString(metadata.workspaceId),
|
|
181
|
+
status,
|
|
182
|
+
tags: readStringArray(metadata.tags),
|
|
183
|
+
chatCount: typeof metadata.chatCount === "number" ? metadata.chatCount : 0,
|
|
184
|
+
artifactCount: typeof metadata.artifactCount === "number" ? metadata.artifactCount : 0,
|
|
185
|
+
lastActivityAt: typeof metadata.lastActivityAt === "number" ? metadata.lastActivityAt : updatedAt,
|
|
186
|
+
_creationTime: typeof topic._creationTime === "number" ? topic._creationTime : createdAt,
|
|
187
|
+
createdAt,
|
|
188
|
+
updatedAt
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function resolveTopicProjectOverlay(ctx, scopeId, options = {}) {
|
|
192
|
+
const topic = await resolveTopicDoc(ctx, scopeId);
|
|
193
|
+
if (!topic) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
if (options.projectLikeOnly !== false && !isProjectLikeTopic(topic)) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
return materializeTopicProjectOverlay(topic, options.idMode);
|
|
200
|
+
}
|
|
201
|
+
async function listTopicProjectOverlays(ctx, options = {}) {
|
|
202
|
+
let allTopics = [];
|
|
203
|
+
if (ctx?.db?.query && typeof ctx.db.query === "function") {
|
|
204
|
+
try {
|
|
205
|
+
allTopics = await ctx.db.query("topics").collect();
|
|
206
|
+
} catch (error) {
|
|
207
|
+
debugGraphPrimitiveFallback(
|
|
208
|
+
"[topicProjectOverlay] Failed to read topics table; falling back to API",
|
|
209
|
+
{ error }
|
|
210
|
+
);
|
|
211
|
+
allTopics = [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (allTopics.length === 0 && typeof ctx.runQuery === "function") {
|
|
215
|
+
allTopics = (await ctx.runQuery(api.topics.list, {}) ?? []) || [];
|
|
216
|
+
}
|
|
217
|
+
return allTopics.filter(
|
|
218
|
+
(topic) => options.projectLikeOnly === false || isProjectLikeTopic(topic)
|
|
219
|
+
).map((topic) => materializeTopicProjectOverlay(topic, options.idMode));
|
|
220
|
+
}
|
|
221
|
+
async function patchTopicProjectOverlay(ctx, scopeId, value) {
|
|
222
|
+
const topic = await resolveTopicDoc(ctx, scopeId);
|
|
223
|
+
if (!topic) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const nextMetadata = { ...readMetadata(topic) };
|
|
227
|
+
const patch = {};
|
|
228
|
+
const topicUpdateArgs = {
|
|
229
|
+
id: String(topic._id)
|
|
230
|
+
};
|
|
231
|
+
for (const [key, rawValue] of Object.entries(value)) {
|
|
232
|
+
switch (key) {
|
|
233
|
+
case "_id":
|
|
234
|
+
case "projectId":
|
|
235
|
+
case "topicId":
|
|
236
|
+
case "legacyProjectId":
|
|
237
|
+
case "storageProjectId":
|
|
238
|
+
break;
|
|
239
|
+
case "name":
|
|
240
|
+
case "description":
|
|
241
|
+
patch[key] = rawValue;
|
|
242
|
+
topicUpdateArgs[key] = rawValue;
|
|
243
|
+
break;
|
|
244
|
+
case "tenantId":
|
|
245
|
+
case "workspaceId":
|
|
246
|
+
case "ownerId":
|
|
247
|
+
throw new Error(
|
|
248
|
+
`patchTopicProjectOverlay cannot mutate ${key} via component-owned topics`
|
|
249
|
+
);
|
|
250
|
+
case "status": {
|
|
251
|
+
const status = coerceStatus(rawValue);
|
|
252
|
+
if (status) {
|
|
253
|
+
patch.status = status;
|
|
254
|
+
topicUpdateArgs.status = status;
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case "visibility": {
|
|
259
|
+
const visibility = coerceVisibility(rawValue);
|
|
260
|
+
if (visibility) {
|
|
261
|
+
patch.visibility = visibility;
|
|
262
|
+
topicUpdateArgs.visibility = visibility;
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case "type": {
|
|
267
|
+
const projectType = readNonEmptyString(rawValue);
|
|
268
|
+
if (projectType) {
|
|
269
|
+
nextMetadata.projectType = projectType;
|
|
270
|
+
} else {
|
|
271
|
+
delete nextMetadata.projectType;
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case "updatedAt":
|
|
276
|
+
case "createdAt":
|
|
277
|
+
break;
|
|
278
|
+
default:
|
|
279
|
+
if (rawValue === void 0) {
|
|
280
|
+
delete nextMetadata[key];
|
|
281
|
+
} else {
|
|
282
|
+
nextMetadata[key] = rawValue;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
patch.updatedAt = Date.now();
|
|
287
|
+
patch.metadata = nextMetadata;
|
|
288
|
+
topicUpdateArgs.metadata = nextMetadata;
|
|
289
|
+
if (typeof ctx.runMutation === "function") {
|
|
290
|
+
try {
|
|
291
|
+
await ctx.runMutation(api.topics.update, topicUpdateArgs);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (!isMissingLucernChildComponentError(error) || !ctx?.db || typeof ctx.db.patch !== "function") {
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
await ctx.db.patch(String(topic._id), patch);
|
|
297
|
+
}
|
|
298
|
+
} else if (ctx?.db && typeof ctx.db.patch === "function") {
|
|
299
|
+
await ctx.db.patch(String(topic._id), patch);
|
|
300
|
+
} else {
|
|
301
|
+
throw new Error(
|
|
302
|
+
"Cannot patch topic without component adapter (ctx.runMutation unavailable)"
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return materializeTopicProjectOverlay({
|
|
306
|
+
...topic,
|
|
307
|
+
...patch,
|
|
308
|
+
metadata: nextMetadata
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/resolvers.ts
|
|
313
|
+
function isMissingLucernChildComponentError2(error) {
|
|
314
|
+
const message = getErrorMessage2(error);
|
|
315
|
+
return message.includes(
|
|
316
|
+
'Child component ComponentName(Identifier("lucern")) not found'
|
|
317
|
+
) || message.includes("Child component") && message.includes("lucern") && message.includes("not found");
|
|
318
|
+
}
|
|
319
|
+
function getErrorMessage2(error) {
|
|
320
|
+
if (error instanceof Error) {
|
|
321
|
+
return error.message;
|
|
322
|
+
}
|
|
323
|
+
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
|
|
324
|
+
return error.message;
|
|
325
|
+
}
|
|
326
|
+
return "unknown error";
|
|
327
|
+
}
|
|
328
|
+
function isAdvisoryTopicPatch(value) {
|
|
329
|
+
const advisoryKeys = /* @__PURE__ */ new Set(["lastActivityAt", "updatedAt"]);
|
|
330
|
+
const keys = Object.keys(value);
|
|
331
|
+
return keys.length > 0 && keys.every((key) => advisoryKeys.has(key));
|
|
332
|
+
}
|
|
333
|
+
async function patchProjectWithTolerance(ctx, projectId, value) {
|
|
334
|
+
try {
|
|
335
|
+
await patchTopicProjectOverlay(ctx, projectId, value);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (!isAdvisoryTopicPatch(value) || !isMissingLucernChildComponentError2(error)) {
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
console.warn(
|
|
341
|
+
"[lucern graph-primitives] Non-fatal advisory topic patch failure",
|
|
342
|
+
{
|
|
343
|
+
projectId,
|
|
344
|
+
keys: Object.keys(value),
|
|
345
|
+
error: getErrorMessage2(error)
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function defaultResolvers() {
|
|
351
|
+
return {
|
|
352
|
+
getProject: (ctx, projectId) => resolveTopicProjectOverlay(ctx, projectId, {
|
|
353
|
+
idMode: "legacy",
|
|
354
|
+
projectLikeOnly: false
|
|
355
|
+
}),
|
|
356
|
+
patchProject: (ctx, projectId, value) => patchProjectWithTolerance(ctx, projectId, value),
|
|
357
|
+
listTopics: (ctx) => listTopicProjectOverlays(ctx, {
|
|
358
|
+
idMode: "legacy"
|
|
359
|
+
}),
|
|
360
|
+
getFinalArtifact: (ctx, artifactId) => ctx.db.get(artifactId)
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
var resolverOverrides = {};
|
|
364
|
+
function resolveGraphPrimitivesAppResolvers(_ctx) {
|
|
365
|
+
return {
|
|
366
|
+
...defaultResolvers(),
|
|
367
|
+
...resolverOverrides
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
var LEGACY_SCOPE_FIELD2 = "graphScopeProjectId";
|
|
371
|
+
function asMappedProjectId(topic) {
|
|
372
|
+
if (!topic) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const directLegacyProjectId = normalizeScopeValue(topic[LEGACY_SCOPE_FIELD2]);
|
|
376
|
+
if (directLegacyProjectId) {
|
|
377
|
+
return directLegacyProjectId;
|
|
378
|
+
}
|
|
379
|
+
const metadata = topic.metadata || {};
|
|
380
|
+
const candidate = metadata[LEGACY_SCOPE_FIELD2] || metadata.legacyProjectId || metadata.projectId || metadata.scopeProjectId;
|
|
381
|
+
return candidate ? candidate : void 0;
|
|
382
|
+
}
|
|
383
|
+
function normalizeScopeValue(value) {
|
|
384
|
+
if (typeof value !== "string") {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const normalized = value.trim();
|
|
388
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
389
|
+
}
|
|
390
|
+
function pickPrimaryTopic(candidates) {
|
|
391
|
+
return [...candidates].sort((a, b) => {
|
|
392
|
+
const depthA = a.depth ?? 9999;
|
|
393
|
+
const depthB = b.depth ?? 9999;
|
|
394
|
+
if (depthA !== depthB) {
|
|
395
|
+
return depthA - depthB;
|
|
396
|
+
}
|
|
397
|
+
const createdA = a.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
398
|
+
const createdB = b.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
399
|
+
if (createdA !== createdB) {
|
|
400
|
+
return createdA - createdB;
|
|
401
|
+
}
|
|
402
|
+
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
403
|
+
})[0];
|
|
404
|
+
}
|
|
405
|
+
async function findTopicsByScopeAlias(ctx, scopeId) {
|
|
406
|
+
try {
|
|
407
|
+
return await ctx.db.query("topics").withIndex(
|
|
408
|
+
"by_graph_scope_project",
|
|
409
|
+
(q) => q.eq(LEGACY_SCOPE_FIELD2, scopeId)
|
|
410
|
+
).collect();
|
|
411
|
+
} catch (error) {
|
|
412
|
+
debugGraphPrimitiveFallback(
|
|
413
|
+
"[topicScope] Failed to resolve scope alias via index",
|
|
414
|
+
{
|
|
415
|
+
error,
|
|
416
|
+
scopeId
|
|
417
|
+
}
|
|
418
|
+
);
|
|
419
|
+
const topics = await ctx.db.query("topics").collect();
|
|
420
|
+
return topics.filter((topic) => {
|
|
421
|
+
const normalizedGlobalId = normalizeScopeValue(topic.globalId);
|
|
422
|
+
const mappedProjectId = asMappedProjectId(topic);
|
|
423
|
+
return String(topic._id) === scopeId || normalizedGlobalId === scopeId || mappedProjectId === scopeId;
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function tryResolveHostTopicById(ctx, topicId) {
|
|
428
|
+
if (typeof ctx.runQuery !== "function") {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
return await ctx.runQuery(api.topics.get, {
|
|
433
|
+
id: topicId
|
|
434
|
+
}) ?? null;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
debugGraphPrimitiveFallback(
|
|
437
|
+
"[topicScope] Failed to resolve topic by host query",
|
|
438
|
+
{
|
|
439
|
+
error,
|
|
440
|
+
topicId
|
|
441
|
+
}
|
|
442
|
+
);
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async function tryResolveHostTopicByLegacyScope(ctx, legacyScopeId) {
|
|
447
|
+
if (typeof ctx.runQuery !== "function") {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
return await ctx.runQuery(api.topics.getByLegacyScopeId, {
|
|
452
|
+
projectId: legacyScopeId
|
|
453
|
+
}) ?? null;
|
|
454
|
+
} catch (error) {
|
|
455
|
+
debugGraphPrimitiveFallback(
|
|
456
|
+
"[topicScope] Failed to resolve topic by legacy scope",
|
|
457
|
+
{
|
|
458
|
+
error,
|
|
459
|
+
legacyScopeId
|
|
460
|
+
}
|
|
461
|
+
);
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async function resolveInheritedWorkspaceScope(ctx, topic) {
|
|
466
|
+
const MAX_DEPTH = 10;
|
|
467
|
+
let tenantId = normalizeScopeValue(topic.tenantId);
|
|
468
|
+
let workspaceId = normalizeScopeValue(topic.workspaceId);
|
|
469
|
+
if (tenantId && workspaceId) {
|
|
470
|
+
return { tenantId, workspaceId };
|
|
471
|
+
}
|
|
472
|
+
let current = topic;
|
|
473
|
+
for (let i = 0; i < MAX_DEPTH && current?.parentTopicId; i++) {
|
|
474
|
+
current = await ctx.db.get(current.parentTopicId);
|
|
475
|
+
if (!current) break;
|
|
476
|
+
if (!tenantId) {
|
|
477
|
+
tenantId = normalizeScopeValue(current.tenantId);
|
|
478
|
+
}
|
|
479
|
+
if (!workspaceId) {
|
|
480
|
+
workspaceId = normalizeScopeValue(current.workspaceId);
|
|
481
|
+
}
|
|
482
|
+
if (tenantId && workspaceId) break;
|
|
483
|
+
}
|
|
484
|
+
return { tenantId, workspaceId };
|
|
485
|
+
}
|
|
486
|
+
async function resolveTopicProjectScope(ctx, args) {
|
|
487
|
+
if (args.topicId) {
|
|
488
|
+
let topic = null;
|
|
489
|
+
try {
|
|
490
|
+
topic = await ctx.db.get(
|
|
491
|
+
args.topicId
|
|
492
|
+
);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
debugGraphPrimitiveFallback(
|
|
495
|
+
"[topicScope] Failed to load topic by direct id",
|
|
496
|
+
{
|
|
497
|
+
error,
|
|
498
|
+
topicId: args.topicId
|
|
499
|
+
}
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
if (!topic) {
|
|
503
|
+
topic = await tryResolveHostTopicById(ctx, String(args.topicId));
|
|
504
|
+
}
|
|
505
|
+
if (!topic) {
|
|
506
|
+
topic = pickPrimaryTopic(
|
|
507
|
+
await findTopicsByScopeAlias(ctx, String(args.topicId))
|
|
508
|
+
) ?? null;
|
|
509
|
+
}
|
|
510
|
+
if (!topic) {
|
|
511
|
+
throw new Error(`Topic not found: ${String(args.topicId)}`);
|
|
512
|
+
}
|
|
513
|
+
const inherited = await resolveInheritedWorkspaceScope(ctx, topic);
|
|
514
|
+
const mapped = asMappedProjectId(topic);
|
|
515
|
+
if (mapped) {
|
|
516
|
+
return {
|
|
517
|
+
topicId: topic._id,
|
|
518
|
+
projectId: mapped,
|
|
519
|
+
tenantId: inherited.tenantId,
|
|
520
|
+
workspaceId: inherited.workspaceId,
|
|
521
|
+
source: "topic"
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
topicId: topic._id,
|
|
526
|
+
tenantId: inherited.tenantId,
|
|
527
|
+
workspaceId: inherited.workspaceId,
|
|
528
|
+
source: "topic"
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (args.projectId) {
|
|
532
|
+
let directTopic = null;
|
|
533
|
+
try {
|
|
534
|
+
directTopic = await ctx.db.get(
|
|
535
|
+
args.projectId
|
|
536
|
+
);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
debugGraphPrimitiveFallback(
|
|
539
|
+
"[topicScope] Failed to load direct project topic",
|
|
540
|
+
{
|
|
541
|
+
error,
|
|
542
|
+
projectId: args.projectId
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
if (directTopic) {
|
|
547
|
+
const inherited = await resolveInheritedWorkspaceScope(ctx, directTopic);
|
|
548
|
+
const mapped = asMappedProjectId(directTopic);
|
|
549
|
+
return {
|
|
550
|
+
topicId: directTopic._id,
|
|
551
|
+
projectId: mapped ?? args.projectId,
|
|
552
|
+
tenantId: inherited.tenantId,
|
|
553
|
+
workspaceId: inherited.workspaceId,
|
|
554
|
+
source: "topic_inferred"
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
directTopic = await tryResolveHostTopicByLegacyScope(ctx, args.projectId);
|
|
558
|
+
if (directTopic) {
|
|
559
|
+
const inherited = await resolveInheritedWorkspaceScope(ctx, directTopic);
|
|
560
|
+
const mapped = asMappedProjectId(directTopic);
|
|
561
|
+
return {
|
|
562
|
+
topicId: directTopic._id,
|
|
563
|
+
projectId: mapped ?? args.projectId,
|
|
564
|
+
tenantId: inherited.tenantId,
|
|
565
|
+
workspaceId: inherited.workspaceId,
|
|
566
|
+
source: "topic_inferred"
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const topics = await findTopicsByScopeAlias(ctx, args.projectId);
|
|
570
|
+
const primary = pickPrimaryTopic(topics);
|
|
571
|
+
if (primary) {
|
|
572
|
+
const inherited = await resolveInheritedWorkspaceScope(ctx, primary);
|
|
573
|
+
return {
|
|
574
|
+
topicId: primary._id,
|
|
575
|
+
projectId: args.projectId,
|
|
576
|
+
tenantId: inherited.tenantId,
|
|
577
|
+
workspaceId: inherited.workspaceId,
|
|
578
|
+
source: "project_mapped_topic"
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
throw new Error(
|
|
582
|
+
`Legacy project scope ${String(args.projectId)} has no mapped topic.`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
throw new Error(
|
|
586
|
+
"Missing scope: provide topicId (preferred) or legacy projectId alias."
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
({
|
|
590
|
+
projectId: v.optional(v.string()),
|
|
591
|
+
topicId: v.optional(v.string())
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// src/epistemicQuestions.helpers.ts
|
|
595
|
+
function generateContentHash(text) {
|
|
596
|
+
const content = `question:${text.trim().toLowerCase().replace(/\s+/g, " ")}`;
|
|
597
|
+
let hash = 5381;
|
|
598
|
+
for (let i = 0; i < content.length; i++) {
|
|
599
|
+
hash = (hash << 5) + hash + content.charCodeAt(i);
|
|
600
|
+
hash &= hash;
|
|
601
|
+
}
|
|
602
|
+
return Math.abs(hash).toString(16).padStart(8, "0");
|
|
603
|
+
}
|
|
604
|
+
function buildTestsEdgeGlobalId(fromGlobalId, toGlobalId) {
|
|
605
|
+
return `edge-${fromGlobalId}-tests-${toGlobalId}`;
|
|
606
|
+
}
|
|
607
|
+
async function markProjectGraphDirty(ctx, projectId, topicId) {
|
|
608
|
+
const normalizedProjectId = typeof projectId === "string" && projectId.trim().length > 0 ? projectId : void 0;
|
|
609
|
+
const normalizedTopicId = typeof topicId === "string" && topicId.trim().length > 0 ? topicId : void 0;
|
|
610
|
+
if (!normalizedProjectId && !normalizedTopicId) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (normalizedProjectId) {
|
|
614
|
+
await ctx.scheduler.runAfter(
|
|
615
|
+
0,
|
|
616
|
+
internal.graphAnalysisCache.markCacheStaleInternal,
|
|
617
|
+
{
|
|
618
|
+
projectId: normalizedProjectId
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
if (normalizedTopicId) {
|
|
623
|
+
await ctx.scheduler.runAfter(
|
|
624
|
+
0,
|
|
625
|
+
internal.graphAnalysisCache.markCacheStaleByTopic,
|
|
626
|
+
{
|
|
627
|
+
topicId: normalizedTopicId
|
|
628
|
+
}
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
await resolveGraphPrimitivesAppResolvers().patchProject(
|
|
632
|
+
ctx,
|
|
633
|
+
normalizedTopicId ?? normalizedProjectId,
|
|
634
|
+
{
|
|
635
|
+
lastActivityAt: Date.now()
|
|
636
|
+
}
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
function dedupeQuestionNodes(nodes) {
|
|
640
|
+
const seen = /* @__PURE__ */ new Set();
|
|
641
|
+
const deduped = [];
|
|
642
|
+
for (const node of nodes) {
|
|
643
|
+
const id = String(node._id);
|
|
644
|
+
if (seen.has(id)) {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
seen.add(id);
|
|
648
|
+
deduped.push(node);
|
|
649
|
+
}
|
|
650
|
+
return deduped;
|
|
651
|
+
}
|
|
652
|
+
function normalizeQuestionTopicId(topicId) {
|
|
653
|
+
return typeof topicId === "string" && topicId.trim().length > 0 ? topicId : void 0;
|
|
654
|
+
}
|
|
655
|
+
function resolveQuestionScopeId(scope) {
|
|
656
|
+
return normalizeQuestionTopicId(scope.topicId) ?? scope.projectId ?? void 0;
|
|
657
|
+
}
|
|
658
|
+
function logQuestionFallback(message, error, context) {
|
|
659
|
+
debugGraphPrimitiveFallback(message, {
|
|
660
|
+
error: formatGraphPrimitiveError(error),
|
|
661
|
+
...context ?? {}
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
async function resolveQuestionScopeOrNull(ctx, args) {
|
|
665
|
+
if (!args.projectId && !args.topicId) {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
return await resolveTopicProjectScope(ctx, {
|
|
670
|
+
projectId: args.projectId ?? void 0,
|
|
671
|
+
topicId: args.topicId ?? void 0
|
|
672
|
+
});
|
|
673
|
+
} catch (error) {
|
|
674
|
+
debugGraphPrimitiveFallback(
|
|
675
|
+
"[epistemicQuestions] Failed to resolve question scope",
|
|
676
|
+
{
|
|
677
|
+
error: formatGraphPrimitiveError(error),
|
|
678
|
+
projectId: args.projectId,
|
|
679
|
+
topicId: args.topicId
|
|
680
|
+
}
|
|
681
|
+
);
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async function getQuestionNodesForScope(ctx, scope, args) {
|
|
686
|
+
const fetchNodes = (query2) => query2.collect();
|
|
687
|
+
const topicNodes = await fetchNodes(
|
|
688
|
+
ctx.db.query("epistemicNodes").withIndex(
|
|
689
|
+
"by_topic_type",
|
|
690
|
+
(q) => q.eq("topicId", scope.topicId).eq("nodeType", "question")
|
|
691
|
+
)
|
|
692
|
+
);
|
|
693
|
+
return dedupeQuestionNodes(topicNodes).filter(
|
|
694
|
+
(node) => questionMatchesScope(node, scope)
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
function questionMatchesScope(node, scope) {
|
|
698
|
+
return scope.topicId !== void 0 && node.topicId === scope.topicId || scope.projectId !== void 0 && node.projectId === scope.projectId;
|
|
699
|
+
}
|
|
700
|
+
function buildLinkedWorktreeMetadata(linkedWorktreeId) {
|
|
701
|
+
return linkedWorktreeId ? {
|
|
702
|
+
linkedWorktreeId
|
|
703
|
+
} : {};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/epistemicQuestions.conviction.ts
|
|
707
|
+
var advanceToConviction = mutation({
|
|
708
|
+
args: {
|
|
709
|
+
questionId: v.id("epistemicNodes"),
|
|
710
|
+
userId: v.string(),
|
|
711
|
+
sprintId: v.optional(v.string()),
|
|
712
|
+
worktreeId: v.optional(v.string())
|
|
713
|
+
},
|
|
714
|
+
returns: permissiveReturn,
|
|
715
|
+
handler: async (ctx, args) => {
|
|
716
|
+
const node = await ctx.db.get(args.questionId);
|
|
717
|
+
if (!node || node.nodeType !== "question") {
|
|
718
|
+
throw new Error("Question not found");
|
|
719
|
+
}
|
|
720
|
+
const meta = node.metadata || {};
|
|
721
|
+
if (meta.convictionStage === "in_conviction" || meta.convictionStage === "scored") {
|
|
722
|
+
throw new Error("Question is already in conviction stage");
|
|
723
|
+
}
|
|
724
|
+
const now = Date.now();
|
|
725
|
+
await ctx.db.patch(args.questionId, {
|
|
726
|
+
updatedAt: now,
|
|
727
|
+
metadata: {
|
|
728
|
+
...meta,
|
|
729
|
+
convictionStage: "in_conviction",
|
|
730
|
+
convictionAdvancedAt: now,
|
|
731
|
+
convictionAdvancedBy: args.userId,
|
|
732
|
+
...buildLinkedWorktreeMetadata(args.worktreeId ?? args.sprintId)
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
try {
|
|
736
|
+
await ctx.db.insert("epistemicAudit", {
|
|
737
|
+
entityType: "question",
|
|
738
|
+
entityId: args.questionId,
|
|
739
|
+
changeType: "status_changed",
|
|
740
|
+
changedAt: now,
|
|
741
|
+
changedBy: args.userId,
|
|
742
|
+
isAgent: false,
|
|
743
|
+
projectId: node.projectId,
|
|
744
|
+
previousState: {
|
|
745
|
+
convictionStage: meta.convictionStage || "researching"
|
|
746
|
+
},
|
|
747
|
+
newState: { convictionStage: "in_conviction" },
|
|
748
|
+
triggeringAction: "question_advanced_to_conviction"
|
|
749
|
+
});
|
|
750
|
+
} catch (e) {
|
|
751
|
+
logQuestionFallback(
|
|
752
|
+
"[epistemicQuestions] Failed to log advanceToConviction audit",
|
|
753
|
+
e,
|
|
754
|
+
{
|
|
755
|
+
questionId: args.questionId,
|
|
756
|
+
projectId: node.projectId
|
|
757
|
+
}
|
|
758
|
+
);
|
|
759
|
+
console.error("[EpistemicAudit] Failed to log advanceToConviction:", e);
|
|
760
|
+
}
|
|
761
|
+
await markProjectGraphDirty(ctx, node.projectId, node.topicId);
|
|
762
|
+
return {
|
|
763
|
+
questionId: args.questionId,
|
|
764
|
+
convictionStage: "in_conviction",
|
|
765
|
+
advancedAt: now
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
var updateConviction = mutation({
|
|
770
|
+
args: {
|
|
771
|
+
questionId: v.id("epistemicNodes"),
|
|
772
|
+
conviction: v.optional(v.number()),
|
|
773
|
+
answerCompleteness: v.optional(
|
|
774
|
+
v.union(
|
|
775
|
+
v.literal("unanswered"),
|
|
776
|
+
v.literal("partial"),
|
|
777
|
+
v.literal("sufficient"),
|
|
778
|
+
v.literal("comprehensive")
|
|
779
|
+
)
|
|
780
|
+
),
|
|
781
|
+
convictionRationale: v.optional(v.string()),
|
|
782
|
+
userId: v.string()
|
|
783
|
+
},
|
|
784
|
+
returns: permissiveReturn,
|
|
785
|
+
handler: async (ctx, args) => {
|
|
786
|
+
const node = await ctx.db.get(args.questionId);
|
|
787
|
+
if (!node || node.nodeType !== "question") {
|
|
788
|
+
throw new Error("Question not found");
|
|
789
|
+
}
|
|
790
|
+
const now = Date.now();
|
|
791
|
+
const meta = node.metadata || {};
|
|
792
|
+
const updates = { ...meta };
|
|
793
|
+
if (args.conviction !== void 0) {
|
|
794
|
+
updates.conviction = Math.max(0, Math.min(1, args.conviction));
|
|
795
|
+
}
|
|
796
|
+
if (args.answerCompleteness !== void 0) {
|
|
797
|
+
updates.answerCompleteness = args.answerCompleteness;
|
|
798
|
+
}
|
|
799
|
+
if (args.convictionRationale !== void 0) {
|
|
800
|
+
updates.convictionRationale = args.convictionRationale;
|
|
801
|
+
}
|
|
802
|
+
updates.convictionUpdatedAt = now;
|
|
803
|
+
updates.convictionUpdatedBy = args.userId;
|
|
804
|
+
await ctx.db.patch(args.questionId, {
|
|
805
|
+
updatedAt: now,
|
|
806
|
+
metadata: updates
|
|
807
|
+
});
|
|
808
|
+
await markProjectGraphDirty(ctx, node.projectId, node.topicId);
|
|
809
|
+
return {
|
|
810
|
+
questionId: args.questionId,
|
|
811
|
+
conviction: updates.conviction ?? meta.conviction,
|
|
812
|
+
answerCompleteness: updates.answerCompleteness ?? meta.answerCompleteness
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
var finalizeConviction = mutation({
|
|
817
|
+
args: {
|
|
818
|
+
questionId: v.id("epistemicNodes"),
|
|
819
|
+
conviction: v.number(),
|
|
820
|
+
answer: v.string(),
|
|
821
|
+
convictionRationale: v.optional(v.string()),
|
|
822
|
+
userId: v.string(),
|
|
823
|
+
answerCompleteness: v.optional(
|
|
824
|
+
v.union(
|
|
825
|
+
v.literal("unanswered"),
|
|
826
|
+
v.literal("partial"),
|
|
827
|
+
v.literal("sufficient"),
|
|
828
|
+
v.literal("comprehensive"),
|
|
829
|
+
v.literal("unanswerable")
|
|
830
|
+
)
|
|
831
|
+
),
|
|
832
|
+
whatWeNeed: v.optional(v.string())
|
|
833
|
+
},
|
|
834
|
+
returns: permissiveReturn,
|
|
835
|
+
handler: async (ctx, args) => {
|
|
836
|
+
const node = await ctx.db.get(args.questionId);
|
|
837
|
+
if (!node || node.nodeType !== "question") {
|
|
838
|
+
throw new Error("Question not found");
|
|
839
|
+
}
|
|
840
|
+
const now = Date.now();
|
|
841
|
+
const clampedConviction = Math.max(0, Math.min(1, args.conviction));
|
|
842
|
+
const isUnanswerable = args.answerCompleteness === "unanswerable";
|
|
843
|
+
const meta = node.metadata || {};
|
|
844
|
+
let completeness = args.answerCompleteness;
|
|
845
|
+
if (!completeness) {
|
|
846
|
+
completeness = clampedConviction >= 0.8 ? "comprehensive" : clampedConviction >= 0.6 ? "sufficient" : clampedConviction >= 0.3 ? "partial" : "unanswered";
|
|
847
|
+
}
|
|
848
|
+
await ctx.db.patch(args.questionId, {
|
|
849
|
+
updatedAt: now,
|
|
850
|
+
metadata: {
|
|
851
|
+
...meta,
|
|
852
|
+
convictionStage: "scored",
|
|
853
|
+
conviction: clampedConviction,
|
|
854
|
+
convictionRationale: args.convictionRationale,
|
|
855
|
+
convictionUpdatedAt: now,
|
|
856
|
+
convictionUpdatedBy: args.userId,
|
|
857
|
+
answer: args.answer,
|
|
858
|
+
answerStatus: "final",
|
|
859
|
+
questionStatus: isUnanswerable ? "blocked" : "answered",
|
|
860
|
+
answeredAt: now,
|
|
861
|
+
answeredBy: args.userId,
|
|
862
|
+
answerCompleteness: completeness,
|
|
863
|
+
whatWeNeed: args.whatWeNeed
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
try {
|
|
867
|
+
await ctx.db.insert("epistemicAudit", {
|
|
868
|
+
entityType: "question",
|
|
869
|
+
entityId: args.questionId,
|
|
870
|
+
changeType: "status_changed",
|
|
871
|
+
changedAt: now,
|
|
872
|
+
changedBy: args.userId,
|
|
873
|
+
isAgent: false,
|
|
874
|
+
projectId: node.projectId,
|
|
875
|
+
previousState: {
|
|
876
|
+
convictionStage: meta.convictionStage || "in_conviction"
|
|
877
|
+
},
|
|
878
|
+
newState: {
|
|
879
|
+
convictionStage: "scored",
|
|
880
|
+
conviction: clampedConviction
|
|
881
|
+
},
|
|
882
|
+
triggeringAction: "question_conviction_finalized"
|
|
883
|
+
});
|
|
884
|
+
} catch (e) {
|
|
885
|
+
logQuestionFallback(
|
|
886
|
+
"[epistemicQuestions] Failed to log finalizeConviction audit",
|
|
887
|
+
e,
|
|
888
|
+
{
|
|
889
|
+
questionId: args.questionId,
|
|
890
|
+
projectId: node.projectId
|
|
891
|
+
}
|
|
892
|
+
);
|
|
893
|
+
console.error("[EpistemicAudit] Failed to log finalizeConviction:", e);
|
|
894
|
+
}
|
|
895
|
+
if (node.projectId || node.topicId) {
|
|
896
|
+
const beliefId = meta.linkedBeliefNodeId || meta.linkedBeliefId || meta.beliefId;
|
|
897
|
+
if (beliefId) {
|
|
898
|
+
try {
|
|
899
|
+
await ctx.scheduler.runAfter(
|
|
900
|
+
0,
|
|
901
|
+
internal.epistemicQuestions.createEvidenceFromScoredQuestion,
|
|
902
|
+
{
|
|
903
|
+
questionNodeId: args.questionId,
|
|
904
|
+
questionText: node.canonicalText || "",
|
|
905
|
+
answerText: args.answer,
|
|
906
|
+
beliefId,
|
|
907
|
+
relatedBeliefIds: meta.relatedBeliefIds || [],
|
|
908
|
+
conviction: clampedConviction,
|
|
909
|
+
rationale: args.convictionRationale || "",
|
|
910
|
+
projectId: node.projectId,
|
|
911
|
+
topicId: normalizeQuestionTopicId(node.topicId),
|
|
912
|
+
userId: args.userId
|
|
913
|
+
}
|
|
914
|
+
);
|
|
915
|
+
} catch (e) {
|
|
916
|
+
logQuestionFallback(
|
|
917
|
+
"[epistemicQuestions] Failed to schedule evidence creation from scored question",
|
|
918
|
+
e,
|
|
919
|
+
{
|
|
920
|
+
questionId: args.questionId,
|
|
921
|
+
beliefId,
|
|
922
|
+
projectId: node.projectId
|
|
923
|
+
}
|
|
924
|
+
);
|
|
925
|
+
console.error(
|
|
926
|
+
"[finalizeConviction] Failed to schedule evidence creation:",
|
|
927
|
+
e
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
await ctx.scheduler.runAfter(0, internal.neo4jSync.syncNodeToNeo4j, {
|
|
933
|
+
nodeId: args.questionId,
|
|
934
|
+
operation: "upsert"
|
|
935
|
+
});
|
|
936
|
+
await markProjectGraphDirty(ctx, node.projectId, node.topicId);
|
|
937
|
+
return {
|
|
938
|
+
questionId: args.questionId,
|
|
939
|
+
convictionStage: "scored",
|
|
940
|
+
conviction: clampedConviction
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
var getByBeliefWithAccess = query({
|
|
945
|
+
args: {
|
|
946
|
+
beliefId: v.string(),
|
|
947
|
+
userId: v.string()
|
|
948
|
+
},
|
|
949
|
+
returns: permissiveReturn,
|
|
950
|
+
handler: async (ctx, args) => {
|
|
951
|
+
let beliefNode = null;
|
|
952
|
+
try {
|
|
953
|
+
beliefNode = await ctx.db.get(args.beliefId);
|
|
954
|
+
} catch (error) {
|
|
955
|
+
debugGraphPrimitiveFallback(
|
|
956
|
+
"[epistemicQuestions] Failed to resolve belief node directly",
|
|
957
|
+
{
|
|
958
|
+
error: formatGraphPrimitiveError(error),
|
|
959
|
+
beliefId: args.beliefId
|
|
960
|
+
}
|
|
961
|
+
);
|
|
962
|
+
try {
|
|
963
|
+
beliefNode = await ctx.db.get(args.beliefId);
|
|
964
|
+
} catch (legacyError) {
|
|
965
|
+
debugGraphPrimitiveFallback(
|
|
966
|
+
"[epistemicQuestions] Failed to resolve legacy belief node",
|
|
967
|
+
{
|
|
968
|
+
error: formatGraphPrimitiveError(legacyError),
|
|
969
|
+
beliefId: args.beliefId
|
|
970
|
+
}
|
|
971
|
+
);
|
|
972
|
+
return [];
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if (!beliefNode) {
|
|
976
|
+
return [];
|
|
977
|
+
}
|
|
978
|
+
const scope = await resolveQuestionScopeOrNull(ctx, {
|
|
979
|
+
projectId: beliefNode.projectId,
|
|
980
|
+
topicId: normalizeQuestionTopicId(beliefNode.topicId)
|
|
981
|
+
});
|
|
982
|
+
const scopeId = scope ? resolveQuestionScopeId(scope) : void 0;
|
|
983
|
+
if (!scope || !scopeId) {
|
|
984
|
+
return [];
|
|
985
|
+
}
|
|
986
|
+
const hasAccess = await checkScopeAccess(ctx, scopeId, args.userId);
|
|
987
|
+
if (!hasAccess) {
|
|
988
|
+
return [];
|
|
989
|
+
}
|
|
990
|
+
const questionNodes = await getQuestionNodesForScope(ctx, scope);
|
|
991
|
+
const beliefIdStr = String(args.beliefId);
|
|
992
|
+
return questionNodes.filter((q) => {
|
|
993
|
+
const meta = q.metadata || {};
|
|
994
|
+
if (String(meta.linkedBeliefNodeId || "") === beliefIdStr) {
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
if (String(meta.linkedBeliefId || "") === beliefIdStr) {
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
if (String(meta.beliefId || "") === beliefIdStr) {
|
|
1001
|
+
return true;
|
|
1002
|
+
}
|
|
1003
|
+
const relatedIds = meta.relatedBeliefIds || [];
|
|
1004
|
+
if (relatedIds.some((id) => String(id) === beliefIdStr)) {
|
|
1005
|
+
return true;
|
|
1006
|
+
}
|
|
1007
|
+
return false;
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
var updateQuestion = mutation({
|
|
1012
|
+
args: {
|
|
1013
|
+
questionId: v.id("epistemicNodes"),
|
|
1014
|
+
question: v.optional(v.string()),
|
|
1015
|
+
category: v.optional(
|
|
1016
|
+
v.union(
|
|
1017
|
+
v.literal("market"),
|
|
1018
|
+
v.literal("competition"),
|
|
1019
|
+
v.literal("product"),
|
|
1020
|
+
v.literal("team"),
|
|
1021
|
+
v.literal("financials"),
|
|
1022
|
+
v.literal("financial"),
|
|
1023
|
+
v.literal("regulatory"),
|
|
1024
|
+
v.literal("timing"),
|
|
1025
|
+
v.literal("customer"),
|
|
1026
|
+
v.literal("technology"),
|
|
1027
|
+
v.literal("distribution"),
|
|
1028
|
+
v.literal("strategic"),
|
|
1029
|
+
v.literal("other")
|
|
1030
|
+
)
|
|
1031
|
+
),
|
|
1032
|
+
priority: v.optional(
|
|
1033
|
+
v.union(v.literal("high"), v.literal("medium"), v.literal("low"))
|
|
1034
|
+
)
|
|
1035
|
+
},
|
|
1036
|
+
returns: permissiveReturn,
|
|
1037
|
+
handler: async (ctx, args) => {
|
|
1038
|
+
const node = await ctx.db.get(args.questionId);
|
|
1039
|
+
if (!node || node.nodeType !== "question") {
|
|
1040
|
+
throw new Error("Question not found");
|
|
1041
|
+
}
|
|
1042
|
+
const now = Date.now();
|
|
1043
|
+
const meta = node.metadata || {};
|
|
1044
|
+
const metaUpdates = { ...meta };
|
|
1045
|
+
if (args.category) {
|
|
1046
|
+
const category = args.category === "financial" ? "financials" : args.category;
|
|
1047
|
+
metaUpdates.category = category;
|
|
1048
|
+
}
|
|
1049
|
+
if (args.priority) {
|
|
1050
|
+
metaUpdates.priority = args.priority;
|
|
1051
|
+
}
|
|
1052
|
+
const patchData = {
|
|
1053
|
+
updatedAt: now,
|
|
1054
|
+
metadata: metaUpdates
|
|
1055
|
+
};
|
|
1056
|
+
if (args.question !== void 0) {
|
|
1057
|
+
patchData.canonicalText = args.question;
|
|
1058
|
+
patchData.contentHash = generateContentHash(args.question);
|
|
1059
|
+
if (node.sourceType === "ai_generated") {
|
|
1060
|
+
patchData.sourceType = "human";
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
await ctx.db.patch(args.questionId, patchData);
|
|
1064
|
+
await ctx.scheduler.runAfter(0, internal.neo4jSync.syncNodeToNeo4j, {
|
|
1065
|
+
nodeId: args.questionId,
|
|
1066
|
+
operation: "upsert"
|
|
1067
|
+
});
|
|
1068
|
+
await markProjectGraphDirty(ctx, node.projectId, node.topicId);
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
var linkToBelief = mutation({
|
|
1072
|
+
args: {
|
|
1073
|
+
questionId: v.id("epistemicNodes"),
|
|
1074
|
+
beliefId: v.string(),
|
|
1075
|
+
// Accept string for backward compat
|
|
1076
|
+
userId: v.string(),
|
|
1077
|
+
testType: v.optional(
|
|
1078
|
+
v.union(
|
|
1079
|
+
v.literal("validates"),
|
|
1080
|
+
v.literal("invalidates"),
|
|
1081
|
+
v.literal("clarifies")
|
|
1082
|
+
)
|
|
1083
|
+
),
|
|
1084
|
+
answerImpact: v.optional(
|
|
1085
|
+
v.object({
|
|
1086
|
+
ifYes: v.string(),
|
|
1087
|
+
ifNo: v.string()
|
|
1088
|
+
})
|
|
1089
|
+
),
|
|
1090
|
+
isPrimaryBelief: v.optional(v.boolean())
|
|
1091
|
+
},
|
|
1092
|
+
returns: permissiveReturn,
|
|
1093
|
+
handler: async (ctx, args) => {
|
|
1094
|
+
const questionNode = await ctx.db.get(args.questionId);
|
|
1095
|
+
if (!questionNode || questionNode.nodeType !== "question") {
|
|
1096
|
+
throw new Error("Question not found");
|
|
1097
|
+
}
|
|
1098
|
+
const questionScopeId = resolveQuestionScopeId({
|
|
1099
|
+
projectId: questionNode.projectId,
|
|
1100
|
+
topicId: normalizeQuestionTopicId(questionNode.topicId)
|
|
1101
|
+
});
|
|
1102
|
+
if (questionScopeId) {
|
|
1103
|
+
const hasAccess = await checkScopeAccess(
|
|
1104
|
+
ctx,
|
|
1105
|
+
questionScopeId,
|
|
1106
|
+
args.userId
|
|
1107
|
+
);
|
|
1108
|
+
if (!hasAccess) {
|
|
1109
|
+
throw new Error("Access denied");
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
let beliefNode = null;
|
|
1113
|
+
try {
|
|
1114
|
+
beliefNode = await ctx.db.get(args.beliefId);
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
debugGraphPrimitiveFallback(
|
|
1117
|
+
"[epistemicQuestions] Failed to resolve belief node directly",
|
|
1118
|
+
{
|
|
1119
|
+
error: formatGraphPrimitiveError(error),
|
|
1120
|
+
beliefId: args.beliefId
|
|
1121
|
+
}
|
|
1122
|
+
);
|
|
1123
|
+
try {
|
|
1124
|
+
beliefNode = await ctx.db.get(args.beliefId);
|
|
1125
|
+
} catch (legacyError) {
|
|
1126
|
+
debugGraphPrimitiveFallback(
|
|
1127
|
+
"[epistemicQuestions] Failed to resolve legacy belief node",
|
|
1128
|
+
{
|
|
1129
|
+
error: formatGraphPrimitiveError(legacyError),
|
|
1130
|
+
beliefId: args.beliefId
|
|
1131
|
+
}
|
|
1132
|
+
);
|
|
1133
|
+
throw new Error("Belief not found");
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (!beliefNode) {
|
|
1137
|
+
throw new Error("Belief not found");
|
|
1138
|
+
}
|
|
1139
|
+
const now = Date.now();
|
|
1140
|
+
const meta = questionNode.metadata || {};
|
|
1141
|
+
const currentBeliefs = meta.relatedBeliefIds || [];
|
|
1142
|
+
const beliefIdStr = String(args.beliefId);
|
|
1143
|
+
const newRelated = currentBeliefs.includes(beliefIdStr) ? currentBeliefs : [...currentBeliefs, beliefIdStr];
|
|
1144
|
+
const metaUpdates = {
|
|
1145
|
+
...meta,
|
|
1146
|
+
relatedBeliefIds: newRelated
|
|
1147
|
+
};
|
|
1148
|
+
if (args.isPrimaryBelief || args.testType) {
|
|
1149
|
+
metaUpdates.linkedBeliefNodeId = args.beliefId;
|
|
1150
|
+
metaUpdates.linkedBeliefId = args.beliefId;
|
|
1151
|
+
metaUpdates.questionType = "belief_test";
|
|
1152
|
+
if (args.testType) {
|
|
1153
|
+
metaUpdates.testType = args.testType;
|
|
1154
|
+
}
|
|
1155
|
+
if (args.answerImpact) {
|
|
1156
|
+
metaUpdates.answerImpact = args.answerImpact;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
await ctx.db.patch(args.questionId, {
|
|
1160
|
+
updatedAt: now,
|
|
1161
|
+
metadata: metaUpdates
|
|
1162
|
+
});
|
|
1163
|
+
try {
|
|
1164
|
+
const beliefGlobalId = beliefNode.globalId;
|
|
1165
|
+
if (beliefGlobalId && questionNode.globalId) {
|
|
1166
|
+
await ctx.scheduler.runAfter(0, internal.neo4jSync.syncNodeToNeo4j, {
|
|
1167
|
+
nodeId: args.questionId,
|
|
1168
|
+
operation: "upsert"
|
|
1169
|
+
});
|
|
1170
|
+
await ctx.scheduler.runAfter(100, internal.neo4jSync.syncNodeToNeo4j, {
|
|
1171
|
+
nodeId: args.beliefId,
|
|
1172
|
+
operation: "upsert"
|
|
1173
|
+
});
|
|
1174
|
+
const edgeGlobalId = buildTestsEdgeGlobalId(
|
|
1175
|
+
questionNode.globalId,
|
|
1176
|
+
beliefGlobalId
|
|
1177
|
+
);
|
|
1178
|
+
await ctx.scheduler.runAfter(250, internal.neo4jEdgeAPI.createEdge, {
|
|
1179
|
+
globalId: edgeGlobalId,
|
|
1180
|
+
fromGlobalId: questionNode.globalId,
|
|
1181
|
+
toGlobalId: beliefGlobalId,
|
|
1182
|
+
edgeType: "tests",
|
|
1183
|
+
context: args.testType || "tests",
|
|
1184
|
+
topicId: questionNode.projectId ? String(questionNode.projectId) : void 0,
|
|
1185
|
+
createdBy: args.userId,
|
|
1186
|
+
fromNodeType: "question",
|
|
1187
|
+
toNodeType: "belief",
|
|
1188
|
+
fromLayer: "L3",
|
|
1189
|
+
toLayer: "L3"
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
} catch (e) {
|
|
1193
|
+
logQuestionFallback(
|
|
1194
|
+
"[epistemicQuestions] Failed to create tests edge",
|
|
1195
|
+
e,
|
|
1196
|
+
{
|
|
1197
|
+
questionId: args.questionId,
|
|
1198
|
+
beliefId: args.beliefId
|
|
1199
|
+
}
|
|
1200
|
+
);
|
|
1201
|
+
console.error("[linkToBelief] Failed to create tests edge:", e);
|
|
1202
|
+
}
|
|
1203
|
+
await markProjectGraphDirty(
|
|
1204
|
+
ctx,
|
|
1205
|
+
questionNode.projectId,
|
|
1206
|
+
questionNode.topicId
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
var linkToInsight = mutation({
|
|
1211
|
+
args: {
|
|
1212
|
+
questionId: v.id("epistemicNodes"),
|
|
1213
|
+
insightId: v.id("epistemicNodes"),
|
|
1214
|
+
userId: v.string(),
|
|
1215
|
+
sprintId: v.optional(v.string()),
|
|
1216
|
+
worktreeId: v.optional(v.string()),
|
|
1217
|
+
relevance: v.optional(v.number()),
|
|
1218
|
+
rationale: v.optional(v.string())
|
|
1219
|
+
},
|
|
1220
|
+
returns: permissiveReturn,
|
|
1221
|
+
handler: async (ctx, args) => {
|
|
1222
|
+
const questionNode = await ctx.db.get(args.questionId);
|
|
1223
|
+
if (!questionNode || questionNode.nodeType !== "question") {
|
|
1224
|
+
throw new Error("Question not found");
|
|
1225
|
+
}
|
|
1226
|
+
const insight = await ctx.db.get(args.insightId);
|
|
1227
|
+
if (!insight) {
|
|
1228
|
+
throw new Error("Insight not found");
|
|
1229
|
+
}
|
|
1230
|
+
const questionScopeId = resolveQuestionScopeId({
|
|
1231
|
+
projectId: questionNode.projectId,
|
|
1232
|
+
topicId: normalizeQuestionTopicId(questionNode.topicId)
|
|
1233
|
+
});
|
|
1234
|
+
if (questionScopeId) {
|
|
1235
|
+
const hasAccess = await checkScopeAccess(
|
|
1236
|
+
ctx,
|
|
1237
|
+
questionScopeId,
|
|
1238
|
+
args.userId
|
|
1239
|
+
);
|
|
1240
|
+
if (!hasAccess) {
|
|
1241
|
+
throw new Error("Access denied");
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
const now = Date.now();
|
|
1245
|
+
const meta = questionNode.metadata || {};
|
|
1246
|
+
const currentInsights = meta.relatedInsightIds || [];
|
|
1247
|
+
const insightIdStr = String(args.insightId);
|
|
1248
|
+
if (!currentInsights.includes(insightIdStr)) {
|
|
1249
|
+
await ctx.db.patch(args.questionId, {
|
|
1250
|
+
updatedAt: now,
|
|
1251
|
+
metadata: {
|
|
1252
|
+
...meta,
|
|
1253
|
+
relatedInsightIds: [...currentInsights, insightIdStr]
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
try {
|
|
1257
|
+
const existingLinks = await ctx.db.query("questionEvidenceLinks").withIndex(
|
|
1258
|
+
"by_questionId",
|
|
1259
|
+
(q) => q.eq("questionId", args.questionId)
|
|
1260
|
+
).collect();
|
|
1261
|
+
const duplicate = existingLinks.find(
|
|
1262
|
+
(link) => String(link.insightId) === String(args.insightId)
|
|
1263
|
+
);
|
|
1264
|
+
if (!duplicate) {
|
|
1265
|
+
await ctx.db.insert("questionEvidenceLinks", {
|
|
1266
|
+
questionId: args.questionId,
|
|
1267
|
+
insightId: args.insightId,
|
|
1268
|
+
helpsAnswer: true,
|
|
1269
|
+
relevance: args.relevance ?? 0.7,
|
|
1270
|
+
rationale: args.rationale || "Linked from research results",
|
|
1271
|
+
createdBy: args.userId,
|
|
1272
|
+
createdAt: now
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
console.error(
|
|
1277
|
+
"[linkToInsight] Failed to create questionEvidenceLink:",
|
|
1278
|
+
e
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
await markProjectGraphDirty(
|
|
1283
|
+
ctx,
|
|
1284
|
+
questionNode.projectId,
|
|
1285
|
+
questionNode.topicId
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
var unlinkInsight = mutation({
|
|
1290
|
+
args: {
|
|
1291
|
+
questionId: v.id("epistemicNodes"),
|
|
1292
|
+
insightId: v.id("epistemicNodes"),
|
|
1293
|
+
userId: v.string()
|
|
1294
|
+
},
|
|
1295
|
+
returns: permissiveReturn,
|
|
1296
|
+
handler: async (ctx, args) => {
|
|
1297
|
+
const questionNode = await ctx.db.get(args.questionId);
|
|
1298
|
+
if (!questionNode || questionNode.nodeType !== "question") {
|
|
1299
|
+
throw new Error("Question not found");
|
|
1300
|
+
}
|
|
1301
|
+
const questionScopeId = resolveQuestionScopeId({
|
|
1302
|
+
projectId: questionNode.projectId,
|
|
1303
|
+
topicId: normalizeQuestionTopicId(questionNode.topicId)
|
|
1304
|
+
});
|
|
1305
|
+
if (questionScopeId) {
|
|
1306
|
+
const hasAccess = await checkScopeAccess(
|
|
1307
|
+
ctx,
|
|
1308
|
+
questionScopeId,
|
|
1309
|
+
args.userId
|
|
1310
|
+
);
|
|
1311
|
+
if (!hasAccess) {
|
|
1312
|
+
throw new Error("Access denied");
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const now = Date.now();
|
|
1316
|
+
const meta = questionNode.metadata || {};
|
|
1317
|
+
const currentInsights = meta.relatedInsightIds || [];
|
|
1318
|
+
const newInsights = currentInsights.filter(
|
|
1319
|
+
(id) => String(id) !== String(args.insightId)
|
|
1320
|
+
);
|
|
1321
|
+
await ctx.db.patch(args.questionId, {
|
|
1322
|
+
updatedAt: now,
|
|
1323
|
+
metadata: {
|
|
1324
|
+
...meta,
|
|
1325
|
+
relatedInsightIds: newInsights
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
try {
|
|
1329
|
+
const links = await ctx.db.query("questionEvidenceLinks").withIndex(
|
|
1330
|
+
"by_questionId",
|
|
1331
|
+
(q) => q.eq("questionId", args.questionId)
|
|
1332
|
+
).collect();
|
|
1333
|
+
for (const link of links) {
|
|
1334
|
+
if (String(link.insightId) === String(args.insightId)) {
|
|
1335
|
+
await ctx.db.delete(link._id);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
logQuestionFallback(
|
|
1340
|
+
"[epistemicQuestions] Failed to remove questionEvidenceLink",
|
|
1341
|
+
e,
|
|
1342
|
+
{
|
|
1343
|
+
questionId: args.questionId,
|
|
1344
|
+
insightId: args.insightId
|
|
1345
|
+
}
|
|
1346
|
+
);
|
|
1347
|
+
console.error(
|
|
1348
|
+
"[unlinkInsight] Failed to remove questionEvidenceLink:",
|
|
1349
|
+
e
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
try {
|
|
1353
|
+
const evidenceNode = await ctx.db.get(args.insightId);
|
|
1354
|
+
if (evidenceNode && evidenceNode.nodeType === "evidence") {
|
|
1355
|
+
const edges = await ctx.db.query("epistemicEdges").withIndex(
|
|
1356
|
+
"by_from_to",
|
|
1357
|
+
(q) => q.eq("fromNodeId", evidenceNode._id).eq("toNodeId", args.questionId)
|
|
1358
|
+
).collect();
|
|
1359
|
+
for (const edge of edges) {
|
|
1360
|
+
if (edge.edgeType === "derived_from") {
|
|
1361
|
+
await ctx.scheduler.runAfter(0, internal.neo4jEdgeAPI.deleteEdge, {
|
|
1362
|
+
globalId: edge.globalId
|
|
1363
|
+
});
|
|
1364
|
+
await ctx.db.delete(edge._id);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
} catch (e) {
|
|
1369
|
+
logQuestionFallback(
|
|
1370
|
+
"[epistemicQuestions] Failed to remove derived edge",
|
|
1371
|
+
e,
|
|
1372
|
+
{
|
|
1373
|
+
questionId: args.questionId,
|
|
1374
|
+
insightId: args.insightId
|
|
1375
|
+
}
|
|
1376
|
+
);
|
|
1377
|
+
console.error("[unlinkInsight] Failed to remove edge:", e);
|
|
1378
|
+
}
|
|
1379
|
+
await markProjectGraphDirty(
|
|
1380
|
+
ctx,
|
|
1381
|
+
questionNode.projectId,
|
|
1382
|
+
questionNode.topicId
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
export { advanceToConviction, finalizeConviction, getByBeliefWithAccess, linkToBelief, linkToInsight, unlinkInsight, updateConviction, updateQuestion };
|
|
1388
|
+
//# sourceMappingURL=epistemicQuestions.conviction.js.map
|
|
1389
|
+
//# sourceMappingURL=epistemicQuestions.conviction.js.map
|