fix: make graph-backed questions authoritative
This commit is contained in:
@@ -23,6 +23,8 @@ That violated the intended one-step reasoning discipline.
|
|||||||
|
|
||||||
The engine should ask one question about one primary concept at a time.
|
The engine should ask one question about one primary concept at a time.
|
||||||
|
|
||||||
|
**The reconstruction model may suggest a question, but only the graph-backed deterministic pipeline may select the user-facing question.**
|
||||||
|
|
||||||
## One-question / one-concept rule
|
## One-question / one-concept rule
|
||||||
|
|
||||||
Every user-facing question should:
|
Every user-facing question should:
|
||||||
@@ -85,6 +87,46 @@ This question:
|
|||||||
- stays graph-backed
|
- stays graph-backed
|
||||||
- avoids pricing or budget before problem existence is established
|
- avoids pricing or budget before problem existence is established
|
||||||
|
|
||||||
|
## Start-case authority rule
|
||||||
|
|
||||||
|
There were previously two question paths during initial analysis:
|
||||||
|
|
||||||
|
- reconstruction model `nextQuestion`
|
||||||
|
- graph-backed unknown selection and question formulation
|
||||||
|
|
||||||
|
The defect was that `startCase` copied the reconstruction `nextQuestion` directly into the normal UI.
|
||||||
|
|
||||||
|
That path is now closed.
|
||||||
|
|
||||||
|
Initial user-facing questioning now follows this pipeline:
|
||||||
|
|
||||||
|
```text
|
||||||
|
reconstruction
|
||||||
|
→ graph build
|
||||||
|
→ unresolved unknown selection
|
||||||
|
→ atomicity assessment
|
||||||
|
→ decomposition if needed
|
||||||
|
→ investigation strategy
|
||||||
|
→ question formulation
|
||||||
|
→ complexity validation
|
||||||
|
→ selectedQuestion
|
||||||
|
```
|
||||||
|
|
||||||
|
The reconstruction question is still retained in diagnostics as provenance, but it is not authoritative.
|
||||||
|
|
||||||
|
## Live result
|
||||||
|
|
||||||
|
Running the commercial-method scenario through the real environment now:
|
||||||
|
|
||||||
|
- succeeds without the enum compatibility failure
|
||||||
|
- does not show the broad reconstruction question in the UI path
|
||||||
|
- surfaces a graph-backed first question instead
|
||||||
|
- keeps the reconstruction question only in diagnostics
|
||||||
|
|
||||||
|
For the tested scenario, the user-facing first question remained:
|
||||||
|
|
||||||
|
> Who experiences this problem?
|
||||||
|
|
||||||
## Remaining limitations
|
## Remaining limitations
|
||||||
|
|
||||||
- question-complexity assessment is still conservative and pattern-based rather than semantic in a richer linguistic sense
|
- question-complexity assessment is still conservative and pattern-based rather than semantic in a richer linguistic sense
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
classifyObservationRelationship,
|
classifyObservationRelationship,
|
||||||
COMPARABILITY_REASONING_NODE_ID,
|
COMPARABILITY_REASONING_NODE_ID,
|
||||||
formulateQuestion,
|
formulateQuestion,
|
||||||
|
formulateTieResolutionQuestion,
|
||||||
} from "./question-formulator.js";
|
} from "./question-formulator.js";
|
||||||
import {
|
import {
|
||||||
graphUpdateSchema,
|
graphUpdateSchema,
|
||||||
@@ -1623,6 +1624,97 @@ function findNodeById(graph, nodeId) {
|
|||||||
return (graph.nodes || []).find((node) => node.id === nodeId) || null;
|
return (graph.nodes || []).find((node) => node.id === nodeId) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function determineGraphBackedQuestion({ situationGraph }) {
|
||||||
|
const graphSnapshot = cloneJsonSafe(situationGraph);
|
||||||
|
let updatedSituationGraph = cloneJsonSafe(situationGraph);
|
||||||
|
let deterministicSelection = selectActiveUnknownCandidate(
|
||||||
|
updatedSituationGraph,
|
||||||
|
updatedSituationGraph.resolvedNodeIds || [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const decompositionResult = runDeterministicDecomposition({
|
||||||
|
graphSnapshot,
|
||||||
|
proposalSnapshot: {
|
||||||
|
addedNodes: [],
|
||||||
|
updatedNodes: [],
|
||||||
|
addedEdges: [],
|
||||||
|
removedEdgeIds: [],
|
||||||
|
resolvedUnknownNodeIds: [],
|
||||||
|
affectedNodeIds: [],
|
||||||
|
selectedQuestion: null,
|
||||||
|
},
|
||||||
|
updatedSituationGraph,
|
||||||
|
reasoningResolution: { reasoningStateOverride: {} },
|
||||||
|
deterministicSelection,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!decompositionResult.success) {
|
||||||
|
return decompositionResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedSituationGraph = decompositionResult.updatedSituationGraph;
|
||||||
|
updatedSituationGraph.reasoningState = buildReasoningState(
|
||||||
|
updatedSituationGraph,
|
||||||
|
);
|
||||||
|
deterministicSelection = selectActiveUnknownCandidate(
|
||||||
|
updatedSituationGraph,
|
||||||
|
updatedSituationGraph.resolvedNodeIds || [],
|
||||||
|
);
|
||||||
|
updatedSituationGraph.activeUnknownNodeId =
|
||||||
|
deterministicSelection?.status === "selected"
|
||||||
|
? deterministicSelection.nodeId
|
||||||
|
: null;
|
||||||
|
updatedSituationGraph.currentSummary = describeGraph(updatedSituationGraph);
|
||||||
|
|
||||||
|
const selectedNode =
|
||||||
|
deterministicSelection?.status === "selected"
|
||||||
|
? findNodeById(updatedSituationGraph, deterministicSelection.nodeId)
|
||||||
|
: null;
|
||||||
|
const formulatedQuestion = selectedNode
|
||||||
|
? formulateQuestion({
|
||||||
|
node: selectedNode,
|
||||||
|
graph: updatedSituationGraph,
|
||||||
|
context: { selectionState: deterministicSelection },
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
updatedSituationGraph,
|
||||||
|
deterministicSelection,
|
||||||
|
selectedQuestion:
|
||||||
|
deterministicSelection?.status === "ambiguous"
|
||||||
|
? {
|
||||||
|
id: "q_tie_resolution",
|
||||||
|
...formulateTieResolutionQuestion({ graph: updatedSituationGraph }),
|
||||||
|
nodeId: null,
|
||||||
|
tiedCandidateIds: deterministicSelection.tiedCandidateIds,
|
||||||
|
}
|
||||||
|
: deterministicSelection?.status === "selected" && formulatedQuestion
|
||||||
|
? {
|
||||||
|
nodeId: deterministicSelection.nodeId,
|
||||||
|
question:
|
||||||
|
formulatedQuestion.question || deterministicSelection.question,
|
||||||
|
reason: formulatedQuestion.reason,
|
||||||
|
strategy: formulatedQuestion.strategy,
|
||||||
|
investigationStrategy: formulatedQuestion.investigationStrategy,
|
||||||
|
questionComplexity: formulatedQuestion.questionComplexity,
|
||||||
|
plainLanguageNormalisations:
|
||||||
|
formulatedQuestion.plainLanguageNormalisations,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
atomicityAssessment: decompositionResult.atomicityAssessment,
|
||||||
|
decompositionPerformed:
|
||||||
|
decompositionResult.decompositionAttempted &&
|
||||||
|
decompositionResult.decompositionAccepted,
|
||||||
|
decompositionAttempted: decompositionResult.decompositionAttempted,
|
||||||
|
selectedUnknownBefore: decompositionResult.selectedUnknownBefore,
|
||||||
|
selectedUnknownAfter: deterministicSelection?.nodeId ?? null,
|
||||||
|
questionComplexityAssessment:
|
||||||
|
formulatedQuestion?.questionComplexity ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function runDeterministicDecomposition({
|
function runDeterministicDecomposition({
|
||||||
graphSnapshot,
|
graphSnapshot,
|
||||||
proposalSnapshot,
|
proposalSnapshot,
|
||||||
|
|||||||
+66
-25
@@ -13,10 +13,14 @@ import {
|
|||||||
updateCaseRequestSchema,
|
updateCaseRequestSchema,
|
||||||
} from "./schema.js";
|
} from "./schema.js";
|
||||||
import { buildInitialGraph, describeGraph } from "./builder.js";
|
import { buildInitialGraph, describeGraph } from "./builder.js";
|
||||||
import { applyValidatedProposal } from "./apply-proposal.js";
|
import {
|
||||||
|
applyValidatedProposal,
|
||||||
|
determineGraphBackedQuestion,
|
||||||
|
} from "./apply-proposal.js";
|
||||||
import { buildGraphUpdatePrompt } from "./prompt-builder.js";
|
import { buildGraphUpdatePrompt } from "./prompt-builder.js";
|
||||||
import {
|
import {
|
||||||
buildReasoningState,
|
buildReasoningState,
|
||||||
|
formulateQuestion,
|
||||||
formulateTieResolutionQuestion,
|
formulateTieResolutionQuestion,
|
||||||
} from "./question-formulator.js";
|
} from "./question-formulator.js";
|
||||||
import { parseGraphUpdateProposal } from "./update-proposal.js";
|
import { parseGraphUpdateProposal } from "./update-proposal.js";
|
||||||
@@ -41,6 +45,13 @@ function buildDiagnostics({
|
|||||||
graph,
|
graph,
|
||||||
graphReferenceValidation,
|
graphReferenceValidation,
|
||||||
unknownSelectionExplanation,
|
unknownSelectionExplanation,
|
||||||
|
reconstructionQuestion,
|
||||||
|
reconstructionQuestionAccepted,
|
||||||
|
reconstructionQuestionRejectionReasons,
|
||||||
|
finalGraphBackedQuestion,
|
||||||
|
selectedUnknownNodeId,
|
||||||
|
decompositionApplied,
|
||||||
|
questionComplexityAssessment,
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
promptVersion: analysis?.promptVersion ?? null,
|
promptVersion: analysis?.promptVersion ?? null,
|
||||||
@@ -54,6 +65,14 @@ function buildDiagnostics({
|
|||||||
compatibilityChanges: analysis?.compatibilityChanges ?? [],
|
compatibilityChanges: analysis?.compatibilityChanges ?? [],
|
||||||
compatibilityWarnings: analysis?.compatibilityWarnings ?? [],
|
compatibilityWarnings: analysis?.compatibilityWarnings ?? [],
|
||||||
unknownSelectionExplanation: unknownSelectionExplanation ?? null,
|
unknownSelectionExplanation: unknownSelectionExplanation ?? null,
|
||||||
|
reconstructionQuestion: reconstructionQuestion ?? null,
|
||||||
|
reconstructionQuestionAccepted: reconstructionQuestionAccepted ?? null,
|
||||||
|
reconstructionQuestionRejectionReasons:
|
||||||
|
reconstructionQuestionRejectionReasons ?? [],
|
||||||
|
finalGraphBackedQuestion: finalGraphBackedQuestion ?? null,
|
||||||
|
selectedUnknownNodeId: selectedUnknownNodeId ?? null,
|
||||||
|
decompositionApplied: decompositionApplied ?? false,
|
||||||
|
questionComplexityAssessment: questionComplexityAssessment ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,23 +282,11 @@ export async function startCase(body) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const currentSummary = describeGraph(initialGraph);
|
const currentSummary = describeGraph(initialGraph);
|
||||||
const deterministicSelection = selectActiveUnknownCandidate(
|
const initialSituationGraph = makeGraph({
|
||||||
{
|
|
||||||
...initialGraph,
|
|
||||||
resolvedNodeIds: [],
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const activeUnknownNodeId =
|
|
||||||
deterministicSelection?.status === "selected"
|
|
||||||
? deterministicSelection.nodeId
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const situationGraph = makeGraph({
|
|
||||||
centralStatement: scenario,
|
centralStatement: scenario,
|
||||||
nodes: initialGraph.nodes,
|
nodes: initialGraph.nodes,
|
||||||
edges: initialGraph.edges,
|
edges: initialGraph.edges,
|
||||||
activeUnknownNodeId,
|
activeUnknownNodeId: null,
|
||||||
resolvedNodeIds: [],
|
resolvedNodeIds: [],
|
||||||
currentSummary,
|
currentSummary,
|
||||||
reasoningState: buildReasoningState({
|
reasoningState: buildReasoningState({
|
||||||
@@ -290,17 +297,20 @@ export async function startCase(body) {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
situationGraphSchema.parse(situationGraph);
|
situationGraphSchema.parse(initialSituationGraph);
|
||||||
|
|
||||||
const graphReferenceValidation = validateGraphReferences(situationGraph);
|
const graphReferenceValidation = validateGraphReferences(
|
||||||
const selectedQuestion =
|
initialSituationGraph,
|
||||||
deterministicSelection?.status === "ambiguous"
|
);
|
||||||
? {
|
const initialQuestionResult = determineGraphBackedQuestion({
|
||||||
id: "q_tie_resolution",
|
situationGraph: initialSituationGraph,
|
||||||
...formulateTieResolutionQuestion({ graph: situationGraph }),
|
});
|
||||||
tiedCandidateIds: deterministicSelection.tiedCandidateIds,
|
const situationGraph = initialQuestionResult.success
|
||||||
}
|
? initialQuestionResult.updatedSituationGraph
|
||||||
: (analysis.nextQuestion ?? null);
|
: initialSituationGraph;
|
||||||
|
const selectedQuestion = initialQuestionResult.success
|
||||||
|
? initialQuestionResult.selectedQuestion
|
||||||
|
: null;
|
||||||
const unknownSelectionExplanation = buildUnknownSelectionDiagnostics(
|
const unknownSelectionExplanation = buildUnknownSelectionDiagnostics(
|
||||||
situationGraph,
|
situationGraph,
|
||||||
[],
|
[],
|
||||||
@@ -315,6 +325,22 @@ export async function startCase(body) {
|
|||||||
graph: situationGraph,
|
graph: situationGraph,
|
||||||
graphReferenceValidation,
|
graphReferenceValidation,
|
||||||
unknownSelectionExplanation,
|
unknownSelectionExplanation,
|
||||||
|
reconstructionQuestion: analysis.nextQuestion?.question ?? null,
|
||||||
|
reconstructionQuestionAccepted: false,
|
||||||
|
reconstructionQuestionRejectionReasons:
|
||||||
|
analysis.nextQuestion?.question != null
|
||||||
|
? [
|
||||||
|
"reconstruction_question_not_authoritative",
|
||||||
|
"graph_backed_pipeline_required",
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
finalGraphBackedQuestion: selectedQuestion?.question ?? null,
|
||||||
|
selectedUnknownNodeId:
|
||||||
|
initialQuestionResult.selectedUnknownAfter ?? null,
|
||||||
|
decompositionApplied:
|
||||||
|
initialQuestionResult.decompositionPerformed ?? false,
|
||||||
|
questionComplexityAssessment:
|
||||||
|
initialQuestionResult.questionComplexityAssessment ?? null,
|
||||||
}),
|
}),
|
||||||
validationErrors: graphReferenceValidation.errors,
|
validationErrors: graphReferenceValidation.errors,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
@@ -330,6 +356,21 @@ export async function startCase(body) {
|
|||||||
graph: situationGraph,
|
graph: situationGraph,
|
||||||
graphReferenceValidation,
|
graphReferenceValidation,
|
||||||
unknownSelectionExplanation,
|
unknownSelectionExplanation,
|
||||||
|
reconstructionQuestion: analysis.nextQuestion?.question ?? null,
|
||||||
|
reconstructionQuestionAccepted: false,
|
||||||
|
reconstructionQuestionRejectionReasons:
|
||||||
|
analysis.nextQuestion?.question != null
|
||||||
|
? [
|
||||||
|
"reconstruction_question_not_authoritative",
|
||||||
|
"graph_backed_pipeline_required",
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
finalGraphBackedQuestion: selectedQuestion?.question ?? null,
|
||||||
|
selectedUnknownNodeId: initialQuestionResult.selectedUnknownAfter ?? null,
|
||||||
|
decompositionApplied:
|
||||||
|
initialQuestionResult.decompositionPerformed ?? false,
|
||||||
|
questionComplexityAssessment:
|
||||||
|
initialQuestionResult.questionComplexityAssessment ?? null,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,41 @@ function makeAnalysisResult(overrides = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeCommercialAnalysisResult(overrides = {}) {
|
||||||
|
return makeAnalysisResult({
|
||||||
|
reconstruction: {
|
||||||
|
summary:
|
||||||
|
"A new reasoning method may become a commercial product, but problem existence and value remain unresolved.",
|
||||||
|
actors: [],
|
||||||
|
systemsOrObjects: [],
|
||||||
|
expectedStates: [],
|
||||||
|
observedStates: [],
|
||||||
|
differences: [],
|
||||||
|
knownTransitions: [],
|
||||||
|
unexplainedTransitions: [],
|
||||||
|
contradictions: [],
|
||||||
|
importantUnknowns: [
|
||||||
|
{
|
||||||
|
id: "unk-commercial",
|
||||||
|
label:
|
||||||
|
"Commercial justification for whether continuing development is commercially justified",
|
||||||
|
description:
|
||||||
|
"Need to know whether this solves a genuine problem, whether people would value it enough to pay for it, and whether it is commercially justified before continuing development.",
|
||||||
|
confidence: "high",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plausibleInterpretations: [],
|
||||||
|
},
|
||||||
|
nextQuestion: {
|
||||||
|
id: "q-commercial",
|
||||||
|
question:
|
||||||
|
"What specific validation metrics, pilot feedback, or competitive benchmarking results have you collected to measure whether the method solves a recognized problem and how target users evaluate its practical utility compared to existing tools?",
|
||||||
|
reason: "Model-proposed broad validation question",
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function makeUpdateGraph() {
|
function makeUpdateGraph() {
|
||||||
const unknown = makeNode({
|
const unknown = makeNode({
|
||||||
id: "n-unknown",
|
id: "n-unknown",
|
||||||
@@ -401,9 +436,15 @@ describe("lib/graph/orchestrator startCase", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null selectedQuestion when analysis has no nextQuestion", async () => {
|
it("returns null selectedQuestion when neither analysis nor graph path yields a question", async () => {
|
||||||
mockAnalyseScenario.mockResolvedValue(
|
mockAnalyseScenario.mockResolvedValue(
|
||||||
makeAnalysisResult({ nextQuestion: undefined }),
|
makeAnalysisResult({
|
||||||
|
nextQuestion: undefined,
|
||||||
|
reconstruction: {
|
||||||
|
...makeAnalysisResult().reconstruction,
|
||||||
|
importantUnknowns: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
const { startCase } = await import("@/lib/graph/orchestrator.js");
|
const { startCase } = await import("@/lib/graph/orchestrator.js");
|
||||||
|
|
||||||
@@ -413,6 +454,40 @@ describe("lib/graph/orchestrator startCase", () => {
|
|||||||
expect(result.selectedQuestion).toBeNull();
|
expect(result.selectedQuestion).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses the graph-backed question path instead of the reconstruction nextQuestion on startCase", async () => {
|
||||||
|
mockAnalyseScenario.mockResolvedValue(makeCommercialAnalysisResult());
|
||||||
|
const { startCase } = await import("@/lib/graph/orchestrator.js");
|
||||||
|
|
||||||
|
const result = await startCase({
|
||||||
|
scenario:
|
||||||
|
"I have developed a new reasoning method that aims to help people determine whether they have enough justified confidence to make a decision.",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.selectedQuestion?.question).toBe(
|
||||||
|
"Who experiences this problem?",
|
||||||
|
);
|
||||||
|
expect(result.selectedQuestion?.nodeId).toBe(
|
||||||
|
result.situationGraph.activeUnknownNodeId,
|
||||||
|
);
|
||||||
|
expect(result.selectedQuestion?.question).not.toContain(
|
||||||
|
"validation metrics, pilot feedback, or competitive benchmarking",
|
||||||
|
);
|
||||||
|
expect(result.diagnostics.reconstructionQuestion).toContain(
|
||||||
|
"validation metrics, pilot feedback, or competitive benchmarking",
|
||||||
|
);
|
||||||
|
expect(result.diagnostics.reconstructionQuestionAccepted).toBe(false);
|
||||||
|
expect(result.diagnostics.reconstructionQuestionRejectionReasons).toContain(
|
||||||
|
"graph_backed_pipeline_required",
|
||||||
|
);
|
||||||
|
expect(result.diagnostics.finalGraphBackedQuestion).toBe(
|
||||||
|
"Who experiences this problem?",
|
||||||
|
);
|
||||||
|
expect(result.diagnostics.selectedUnknownNodeId).toBe(
|
||||||
|
result.situationGraph.activeUnknownNodeId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("includes compatibility diagnostics when provided by analysis", async () => {
|
it("includes compatibility diagnostics when provided by analysis", async () => {
|
||||||
mockAnalyseScenario.mockResolvedValue(
|
mockAnalyseScenario.mockResolvedValue(
|
||||||
makeAnalysisResult({
|
makeAnalysisResult({
|
||||||
@@ -1093,16 +1168,20 @@ describe("lib/graph/orchestrator startCase", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("startCase behaviour remains unchanged", async () => {
|
it("startCase no longer copies analysis nextQuestion directly when a graph-backed question exists", async () => {
|
||||||
mockAnalyseScenario.mockResolvedValue(makeAnalysisResult());
|
mockAnalyseScenario.mockResolvedValue(makeAnalysisResult());
|
||||||
const { startCase } = await import("@/lib/graph/orchestrator.js");
|
const { startCase } = await import("@/lib/graph/orchestrator.js");
|
||||||
|
|
||||||
const result = await startCase({ scenario: "Scenario text" });
|
const result = await startCase({ scenario: "Scenario text" });
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.selectedQuestion).toEqual({
|
expect(result.selectedQuestion).toMatchObject({
|
||||||
id: "q-1",
|
nodeId: result.situationGraph.activeUnknownNodeId,
|
||||||
question: "What denominator is being used for the complaint rate?",
|
question:
|
||||||
|
"What would clarify need the denominator for complaint rate in this situation?",
|
||||||
});
|
});
|
||||||
|
expect(result.diagnostics.reconstructionQuestion).toBe(
|
||||||
|
"What denominator is being used for the complaint rate?",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user