refactor(detekt): extract magic-number constants, wrap long lines, drop legit suppressions
Mechanical detekt cleanup across apps/core/infrastructure (behavior-preserving): - MagicNumber 62->10: named constants + removed 'legit' MagicNumber suppressions (Tier level from ordinal, ReplayInferenceProvider CHARS_PER_TOKEN, ConfigLoader DEFAULT_* consts, capability scores, HTTP ranges, column widths, etc.) - MaxLineLength cut to ~0 in production modules (line wraps, no logic change) - Dropped one dead parameter (ConfigLoader.parseArray lineNum) Structural findings (ReturnCount/LongMethod/Complexity/LargeClass) left for a deliberate decomposition pass. Full build + detekt gate green.
This commit is contained in:
@@ -19,7 +19,11 @@ package com.correx.core.artifacts.kind
|
||||
object KindContractTable {
|
||||
|
||||
/** A single assertion template — a kind's requirement before it is bound to a concrete path. */
|
||||
private data class Template(val id: String, val evaluator: AssertionEvaluator, val args: Map<String, String> = emptyMap())
|
||||
private data class Template(
|
||||
val id: String,
|
||||
val evaluator: AssertionEvaluator,
|
||||
val args: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
private val FLOOR = listOf(
|
||||
Template("file_exists", AssertionEvaluator.FS),
|
||||
|
||||
@@ -104,7 +104,10 @@ internal object SimpleToml {
|
||||
ch == '\\' && inQuotes && quoteChar == '"' -> { current.append(ch); escaped = true }
|
||||
(ch == '"' || ch == '\'') && !inQuotes -> { inQuotes = true; quoteChar = ch; current.append(ch) }
|
||||
ch == quoteChar && inQuotes -> { inQuotes = false; current.append(ch) }
|
||||
ch == ',' && !inQuotes -> { result.add(stripQuotes(current.toString().trim())); current = StringBuilder() }
|
||||
ch == ',' && !inQuotes -> {
|
||||
result.add(stripQuotes(current.toString().trim()))
|
||||
current = StringBuilder()
|
||||
}
|
||||
else -> current.append(ch)
|
||||
}
|
||||
}
|
||||
@@ -181,6 +184,46 @@ object ProfileLoader {
|
||||
}
|
||||
|
||||
object ConfigLoader {
|
||||
private const val DEFAULT_SERVER_PORT = 8080
|
||||
private const val DEFAULT_SESSION_LIST_LIMIT = 5
|
||||
private const val DEFAULT_EMBEDDER_DIMENSION = 1536
|
||||
private const val DEFAULT_L3_DIM = 1536
|
||||
private const val DEFAULT_L3_BIT_WIDTH = 4
|
||||
private const val DEFAULT_GENERATION_TEMPERATURE = 0.7
|
||||
private const val DEFAULT_GENERATION_TOP_P = 0.9
|
||||
private const val DEFAULT_GENERATION_MAX_TOKENS = 512
|
||||
private const val DEFAULT_NARRATION_TEMPERATURE = 0.7
|
||||
private const val DEFAULT_NARRATION_TOP_P = 0.9
|
||||
private const val DEFAULT_NARRATION_MAX_TOKENS = 4096
|
||||
private const val DEFAULT_NARRATION_MAX_PER_RUN = 100
|
||||
private const val DEFAULT_CONVERSATION_KEEP_LAST = 6
|
||||
private const val DEFAULT_RETRIEVAL_K = 5
|
||||
private const val DEFAULT_TOKEN_BUDGET = 4096
|
||||
private const val DEFAULT_MODEL_CONTEXT_SIZE = 8192
|
||||
private const val DEFAULT_PROJECT_MEMORY_K = 5
|
||||
private const val DEFAULT_PROJECT_MAX_DEPTH = 4
|
||||
private const val DEFAULT_PROJECT_INJECT_TOP_K = 30
|
||||
private const val DEFAULT_STAGE_TIMEOUT_MS = 180_000L
|
||||
private const val DEFAULT_JOURNAL_COMPACTION_TOKEN_THRESHOLD = 2_000
|
||||
private const val DEFAULT_RESUME_ABANDONED_MAX_AGE_MINUTES = 1_440L
|
||||
private const val DEFAULT_COMPRESSION_LEVEL = 4
|
||||
private const val DEFAULT_MAX_TOOL_ROUNDS = 30
|
||||
private const val DEFAULT_READ_LOOP_NUDGE_THRESHOLD = 3
|
||||
private const val DEFAULT_REJECTION_LOOP_NUDGE_THRESHOLD = 3
|
||||
private const val DEFAULT_MAX_FEEDBACK_ISSUES = 3
|
||||
private const val DEFAULT_REPO_MAP_INJECT_TOP_K = 30
|
||||
private const val DEFAULT_REPO_MAP_FILES_PER_DIR = 8
|
||||
private const val DEFAULT_DOCS_CATALOG_MAX = 20
|
||||
private const val DEFAULT_MAX_CLARIFICATION_ROUNDS = 3
|
||||
private const val DEFAULT_REVIEW_BLOCK_MIN_CONFIDENCE = 0.7
|
||||
private const val DEFAULT_REVIEW_BLOCK_RETRY_CAP = 20
|
||||
private const val DEFAULT_MAX_REFINEMENT = 3
|
||||
private const val DEFAULT_RECOVERY_ROUTE_BUDGET = 2
|
||||
private const val DEFAULT_INTENT_ROUTE_BUDGET = 2
|
||||
private const val DEFAULT_MODELS_PORT = 10000
|
||||
private const val DEFAULT_SAMPLING_TOP_P = 1.0
|
||||
private const val DEFAULT_SAMPLING_TEMPERATURE = 0.7
|
||||
|
||||
fun load(): CorrexConfig {
|
||||
val path = configPath()
|
||||
if (!Files.exists(path)) {
|
||||
@@ -302,7 +345,7 @@ object ConfigLoader {
|
||||
}
|
||||
// JSON-style array: ["item1", "item2"]
|
||||
valueStr.startsWith("[") && valueStr.endsWith("]") -> {
|
||||
parseArray(valueStr, lineNum)
|
||||
parseArray(valueStr)
|
||||
}
|
||||
// CSV fallback for backward compat (detect by "," and "=" pattern without brackets)
|
||||
valueStr.contains(",") && valueStr.contains("=") && !valueStr.startsWith("\"") -> {
|
||||
@@ -336,7 +379,7 @@ object ConfigLoader {
|
||||
return result
|
||||
}
|
||||
|
||||
private fun parseArray(arrayStr: String, lineNum: Int): List<String> {
|
||||
private fun parseArray(arrayStr: String): List<String> {
|
||||
val result = mutableListOf<String>()
|
||||
val content = arrayStr.substring(1, arrayStr.length - 1).trim()
|
||||
if (content.isEmpty()) return result
|
||||
@@ -430,12 +473,12 @@ object ConfigLoader {
|
||||
|
||||
val server = ServerConfig(
|
||||
host = asString(serverSection["host"], "localhost"),
|
||||
port = asInt(serverSection["port"], 8080),
|
||||
port = asInt(serverSection["port"], DEFAULT_SERVER_PORT),
|
||||
)
|
||||
|
||||
val tui = TuiConfig(
|
||||
theme = asString(tuiSection["theme"], "dark"),
|
||||
sessionListLimit = asInt(tuiSection["session_list_limit"], 5),
|
||||
sessionListLimit = asInt(tuiSection["session_list_limit"], DEFAULT_SESSION_LIST_LIMIT),
|
||||
)
|
||||
|
||||
val cli = CliConfig(
|
||||
@@ -447,7 +490,9 @@ object ConfigLoader {
|
||||
val shellEnabled = when {
|
||||
toolsShellSection.containsKey("enabled") -> asBoolean(toolsShellSection["enabled"], true)
|
||||
toolsSection.containsKey("shell_enabled") -> {
|
||||
System.err.println("Warning: 'shell_enabled' in [tools] is deprecated, use [tools.shell] enabled instead")
|
||||
System.err.println(
|
||||
"Warning: 'shell_enabled' in [tools] is deprecated, use [tools.shell] enabled instead"
|
||||
)
|
||||
asBoolean(toolsSection["shell_enabled"], true)
|
||||
}
|
||||
else -> true
|
||||
@@ -456,7 +501,9 @@ object ConfigLoader {
|
||||
val fileReadEnabled = when {
|
||||
toolsFileReadSection.containsKey("enabled") -> asBoolean(toolsFileReadSection["enabled"], true)
|
||||
toolsSection.containsKey("file_read_enabled") -> {
|
||||
System.err.println("Warning: 'file_read_enabled' in [tools] is deprecated, use [tools.file_read] enabled instead")
|
||||
System.err.println(
|
||||
"Warning: 'file_read_enabled' in [tools] is deprecated, use [tools.file_read] enabled instead"
|
||||
)
|
||||
asBoolean(toolsSection["file_read_enabled"], true)
|
||||
}
|
||||
else -> true
|
||||
@@ -465,7 +512,9 @@ object ConfigLoader {
|
||||
val fileWriteEnabled = when {
|
||||
toolsFileWriteSection.containsKey("enabled") -> asBoolean(toolsFileWriteSection["enabled"], true)
|
||||
toolsSection.containsKey("file_write_enabled") -> {
|
||||
System.err.println("Warning: 'file_write_enabled' in [tools] is deprecated, use [tools.file_write] enabled instead")
|
||||
System.err.println(
|
||||
"Warning: 'file_write_enabled' in [tools] is deprecated, use [tools.file_write] enabled instead"
|
||||
)
|
||||
asBoolean(toolsSection["file_write_enabled"], true)
|
||||
}
|
||||
else -> true
|
||||
@@ -474,7 +523,9 @@ object ConfigLoader {
|
||||
val fileEditEnabled = when {
|
||||
toolsFileEditSection.containsKey("enabled") -> asBoolean(toolsFileEditSection["enabled"], true)
|
||||
toolsSection.containsKey("file_edit_enabled") -> {
|
||||
System.err.println("Warning: 'file_edit_enabled' in [tools] is deprecated, use [tools.file_edit] enabled instead")
|
||||
System.err.println(
|
||||
"Warning: 'file_edit_enabled' in [tools] is deprecated, use [tools.file_edit] enabled instead"
|
||||
)
|
||||
asBoolean(toolsSection["file_edit_enabled"], true)
|
||||
}
|
||||
else -> true
|
||||
@@ -491,7 +542,9 @@ object ConfigLoader {
|
||||
val1 is String -> {
|
||||
// Backward compat: check if it's CSV or already a single item
|
||||
if (val1.contains(",")) {
|
||||
System.err.println("Warning: CSV format 'shell_allowed_executables' is deprecated, use JSON array instead")
|
||||
System.err.println(
|
||||
"Warning: CSV format 'shell_allowed_executables' is deprecated, use JSON array instead"
|
||||
)
|
||||
val1.split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
} else {
|
||||
listOf(val1)
|
||||
@@ -547,7 +600,7 @@ object ConfigLoader {
|
||||
|
||||
val embedder = EmbedderConfig(
|
||||
backend = asString(routerEmbedderSection["backend"], "noop"),
|
||||
dimension = asInt(routerEmbedderSection["dimension"], 1536),
|
||||
dimension = asInt(routerEmbedderSection["dimension"], DEFAULT_EMBEDDER_DIMENSION),
|
||||
url = asStringOrNull(routerEmbedderSection["url"]),
|
||||
modelId = asStringOrNull(routerEmbedderSection["model_id"]),
|
||||
)
|
||||
@@ -557,30 +610,30 @@ object ConfigLoader {
|
||||
persistPath = asStringOrNull(routerL3Section["persist_path"]),
|
||||
pythonExecutable = asString(routerL3Section["python_executable"], "python3"),
|
||||
scriptPath = asStringOrNull(routerL3Section["script_path"]),
|
||||
dim = asInt(routerL3Section["dim"], 1536),
|
||||
bitWidth = asInt(routerL3Section["bit_width"], 4),
|
||||
dim = asInt(routerL3Section["dim"], DEFAULT_L3_DIM),
|
||||
bitWidth = asInt(routerL3Section["bit_width"], DEFAULT_L3_BIT_WIDTH),
|
||||
)
|
||||
|
||||
val generation = GenerationSettings(
|
||||
temperature = asDouble(routerGenerationSection["temperature"], 0.7),
|
||||
topP = asDouble(routerGenerationSection["top_p"], 0.9),
|
||||
maxTokens = asInt(routerGenerationSection["max_tokens"], 512),
|
||||
temperature = asDouble(routerGenerationSection["temperature"], DEFAULT_GENERATION_TEMPERATURE),
|
||||
topP = asDouble(routerGenerationSection["top_p"], DEFAULT_GENERATION_TOP_P),
|
||||
maxTokens = asInt(routerGenerationSection["max_tokens"], DEFAULT_GENERATION_MAX_TOKENS),
|
||||
)
|
||||
|
||||
val narration = NarrationSettings(
|
||||
temperature = asDouble(routerNarrationSection["temperature"], 0.7),
|
||||
topP = asDouble(routerNarrationSection["top_p"], 0.9),
|
||||
maxTokens = asInt(routerNarrationSection["max_tokens"], 4096),
|
||||
maxPerRun = asInt(routerNarrationSection["max_per_run"], 100),
|
||||
temperature = asDouble(routerNarrationSection["temperature"], DEFAULT_NARRATION_TEMPERATURE),
|
||||
topP = asDouble(routerNarrationSection["top_p"], DEFAULT_NARRATION_TOP_P),
|
||||
maxTokens = asInt(routerNarrationSection["max_tokens"], DEFAULT_NARRATION_MAX_TOKENS),
|
||||
maxPerRun = asInt(routerNarrationSection["max_per_run"], DEFAULT_NARRATION_MAX_PER_RUN),
|
||||
modelId = asStringOrNull(routerNarrationSection["model_id"]),
|
||||
)
|
||||
|
||||
val router = TalkieConfig(
|
||||
embedder = embedder,
|
||||
l3 = l3,
|
||||
conversationKeepLast = asInt(routerSection["conversation_keep_last"], 6),
|
||||
retrievalK = asInt(routerSection["retrieval_k"], 5),
|
||||
tokenBudget = asInt(routerSection["token_budget"], 4096),
|
||||
conversationKeepLast = asInt(routerSection["conversation_keep_last"], DEFAULT_CONVERSATION_KEEP_LAST),
|
||||
retrievalK = asInt(routerSection["retrieval_k"], DEFAULT_RETRIEVAL_K),
|
||||
tokenBudget = asInt(routerSection["token_budget"], DEFAULT_TOKEN_BUDGET),
|
||||
generation = generation,
|
||||
narration = narration,
|
||||
)
|
||||
@@ -588,7 +641,7 @@ object ConfigLoader {
|
||||
val models = modelsList.mapNotNull { modelMap ->
|
||||
val id = asString(modelMap["id"]) ?: return@mapNotNull null
|
||||
val modelPath = asString(modelMap["model_path"]) ?: return@mapNotNull null
|
||||
val contextSize = asInt(modelMap["context_size"], 8192)
|
||||
val contextSize = asInt(modelMap["context_size"], DEFAULT_MODEL_CONTEXT_SIZE)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val params = (modelMap["params"] as? Map<String, Any>)
|
||||
?.mapValues { it.value.toString() }
|
||||
@@ -614,42 +667,53 @@ object ConfigLoader {
|
||||
val project = ProjectConfig(
|
||||
enabled = asBoolean(projectSection["enabled"], false),
|
||||
root = asString(projectSection["root"], ""),
|
||||
memoryK = asInt(projectSection["memory_k"], 5),
|
||||
maxDepth = asInt(projectSection["max_depth"], 4),
|
||||
memoryK = asInt(projectSection["memory_k"], DEFAULT_PROJECT_MEMORY_K),
|
||||
maxDepth = asInt(projectSection["max_depth"], DEFAULT_PROJECT_MAX_DEPTH),
|
||||
ignoreGlobs = asStringList(projectSection["ignore_globs"]).ifEmpty { ProjectConfig.DEFAULT_IGNORES },
|
||||
injectTopK = asInt(projectSection["inject_top_k"], 30),
|
||||
injectTopK = asInt(projectSection["inject_top_k"], DEFAULT_PROJECT_INJECT_TOP_K),
|
||||
)
|
||||
|
||||
val orchestrationSection = sections["orchestration"] ?: emptyMap()
|
||||
val orchestration = OrchestrationKnobs(
|
||||
stageTimeoutMs = asLong(orchestrationSection["stage_timeout_ms"], 180_000),
|
||||
journalCompactionTokenThreshold =
|
||||
asInt(orchestrationSection["journal_compaction_token_threshold"], 2_000),
|
||||
resumeAbandonedMaxAgeMinutes =
|
||||
asLong(orchestrationSection["resume_abandoned_max_age_minutes"], 1_440),
|
||||
compressionLevel = asInt(orchestrationSection["compression_level"], 4),
|
||||
stageTimeoutMs = asLong(orchestrationSection["stage_timeout_ms"], DEFAULT_STAGE_TIMEOUT_MS),
|
||||
journalCompactionTokenThreshold = asInt(
|
||||
orchestrationSection["journal_compaction_token_threshold"],
|
||||
DEFAULT_JOURNAL_COMPACTION_TOKEN_THRESHOLD,
|
||||
),
|
||||
resumeAbandonedMaxAgeMinutes = asLong(
|
||||
orchestrationSection["resume_abandoned_max_age_minutes"],
|
||||
DEFAULT_RESUME_ABANDONED_MAX_AGE_MINUTES,
|
||||
),
|
||||
compressionLevel = asInt(orchestrationSection["compression_level"], DEFAULT_COMPRESSION_LEVEL),
|
||||
tokenPrunerUrl =
|
||||
asString(orchestrationSection["token_pruner_url"], "http://127.0.0.1:8199"),
|
||||
maxToolRounds = asInt(orchestrationSection["max_tool_rounds"], 30),
|
||||
readLoopNudgeThreshold = asInt(orchestrationSection["read_loop_nudge_threshold"], 3),
|
||||
rejectionLoopNudgeThreshold = asInt(orchestrationSection["rejection_loop_nudge_threshold"], 3),
|
||||
maxFeedbackIssues = asInt(orchestrationSection["max_feedback_issues"], 3),
|
||||
repoMapInjectTopK = asInt(orchestrationSection["repo_map_inject_top_k"], 30),
|
||||
repoMapFilesPerDir = asInt(orchestrationSection["repo_map_files_per_dir"], 8),
|
||||
docsCatalogMax = asInt(orchestrationSection["docs_catalog_max"], 20),
|
||||
maxClarificationRounds = asInt(orchestrationSection["max_clarification_rounds"], 3),
|
||||
reviewBlockMinConfidence = asDouble(orchestrationSection["review_block_min_confidence"], 0.7),
|
||||
reviewBlockRetryCap = asInt(orchestrationSection["review_block_retry_cap"], 20),
|
||||
defaultMaxRefinement = asInt(orchestrationSection["default_max_refinement"], 3),
|
||||
recoveryRouteBudget = asInt(orchestrationSection["recovery_route_budget"], 2),
|
||||
intentRouteBudget = asInt(orchestrationSection["intent_route_budget"], 2),
|
||||
maxToolRounds = asInt(orchestrationSection["max_tool_rounds"], DEFAULT_MAX_TOOL_ROUNDS),
|
||||
readLoopNudgeThreshold =
|
||||
asInt(orchestrationSection["read_loop_nudge_threshold"], DEFAULT_READ_LOOP_NUDGE_THRESHOLD),
|
||||
rejectionLoopNudgeThreshold = asInt(
|
||||
orchestrationSection["rejection_loop_nudge_threshold"],
|
||||
DEFAULT_REJECTION_LOOP_NUDGE_THRESHOLD,
|
||||
),
|
||||
maxFeedbackIssues = asInt(orchestrationSection["max_feedback_issues"], DEFAULT_MAX_FEEDBACK_ISSUES),
|
||||
repoMapInjectTopK = asInt(orchestrationSection["repo_map_inject_top_k"], DEFAULT_REPO_MAP_INJECT_TOP_K),
|
||||
repoMapFilesPerDir =
|
||||
asInt(orchestrationSection["repo_map_files_per_dir"], DEFAULT_REPO_MAP_FILES_PER_DIR),
|
||||
docsCatalogMax = asInt(orchestrationSection["docs_catalog_max"], DEFAULT_DOCS_CATALOG_MAX),
|
||||
maxClarificationRounds =
|
||||
asInt(orchestrationSection["max_clarification_rounds"], DEFAULT_MAX_CLARIFICATION_ROUNDS),
|
||||
reviewBlockMinConfidence =
|
||||
asDouble(orchestrationSection["review_block_min_confidence"], DEFAULT_REVIEW_BLOCK_MIN_CONFIDENCE),
|
||||
reviewBlockRetryCap = asInt(orchestrationSection["review_block_retry_cap"], DEFAULT_REVIEW_BLOCK_RETRY_CAP),
|
||||
defaultMaxRefinement = asInt(orchestrationSection["default_max_refinement"], DEFAULT_MAX_REFINEMENT),
|
||||
recoveryRouteBudget = asInt(orchestrationSection["recovery_route_budget"], DEFAULT_RECOVERY_ROUTE_BUDGET),
|
||||
intentRouteBudget = asInt(orchestrationSection["intent_route_budget"], DEFAULT_INTENT_ROUTE_BUDGET),
|
||||
)
|
||||
|
||||
val modelsSettings = ModelsSettings(
|
||||
defaultModel = asStringOrNull(modelsSection["default_model"]),
|
||||
llamaServerBin = asString(modelsSection["llama_server_bin"], "llama-server"),
|
||||
host = asString(modelsSection["host"], "127.0.0.1"),
|
||||
port = asInt(modelsSection["port"], 10000),
|
||||
port = asInt(modelsSection["port"], DEFAULT_MODELS_PORT),
|
||||
)
|
||||
|
||||
val artifacts = artifactsList.mapNotNull { artifactMap ->
|
||||
@@ -664,8 +728,8 @@ object ConfigLoader {
|
||||
|
||||
val samplingSection = sections["sampling"] ?: emptyMap()
|
||||
val sampling = SamplingConfig(
|
||||
temperature = asDouble(samplingSection["temperature"], 0.7),
|
||||
topP = asDouble(samplingSection["top_p"], 1.0),
|
||||
temperature = asDouble(samplingSection["temperature"], DEFAULT_SAMPLING_TEMPERATURE),
|
||||
topP = asDouble(samplingSection["top_p"], DEFAULT_SAMPLING_TOP_P),
|
||||
topK = samplingSection["top_k"]?.let { asInt(it) },
|
||||
minP = samplingSection["min_p"]?.let { asDouble(it) },
|
||||
repeatPenalty = samplingSection["repeat_penalty"]?.let { asDouble(it) },
|
||||
|
||||
@@ -184,7 +184,11 @@ object EditableConfig {
|
||||
"orchestration.resume_abandoned_max_age_minutes", ConfigFieldType.LONG,
|
||||
getString = { it.orchestration.resumeAbandonedMaxAgeMinutes.toString() },
|
||||
withString = { c, v ->
|
||||
orc(c) { it.copy(resumeAbandonedMaxAgeMinutes = lng("orchestration.resume_abandoned_max_age_minutes", v)) }
|
||||
orc(c) {
|
||||
it.copy(
|
||||
resumeAbandonedMaxAgeMinutes = lng("orchestration.resume_abandoned_max_age_minutes", v)
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -17,7 +17,9 @@ object OperatorProfileWriter {
|
||||
}
|
||||
|
||||
val prefs = profile.preferences
|
||||
val hasPrefs = prefs.approvalMode.isNotBlank() || prefs.preferredModels.isNotEmpty() || prefs.conventions.isNotEmpty()
|
||||
val hasPrefs = prefs.approvalMode.isNotBlank() ||
|
||||
prefs.preferredModels.isNotEmpty() ||
|
||||
prefs.conventions.isNotEmpty()
|
||||
if (hasPrefs) {
|
||||
if (b.isNotEmpty()) b.append('\n')
|
||||
b.append("[preferences]\n")
|
||||
|
||||
+5
-2
@@ -97,8 +97,11 @@ class DefaultContextPackBuilder(
|
||||
// Stage 1 (FORMAT_COMPRESS): lossless json→dotted-line compaction of structured entries.
|
||||
val formatted = if (policy.enabled(CompressionStage.FORMAT_COMPRESS)) {
|
||||
deduped.map { entry ->
|
||||
if (classifier.classify(entry) == ContextClass.STRUCTURED) reencode(entry, formatCompressor.compress(entry.content))
|
||||
else entry
|
||||
if (classifier.classify(entry) == ContextClass.STRUCTURED) {
|
||||
reencode(entry, formatCompressor.compress(entry.content))
|
||||
} else {
|
||||
entry
|
||||
}
|
||||
}
|
||||
} else deduped
|
||||
|
||||
|
||||
@@ -142,7 +142,11 @@ class CompressionPipelineStagesTest {
|
||||
// Instruction-doc pruning is retired: curated/procedural docs (project profile, AGENTS.md
|
||||
// how-to) must reach the model verbatim — token-pruning fused them into unparseable soup.
|
||||
val pruner = object : com.correx.core.context.compression.TokenPruner {
|
||||
override suspend fun prune(content: String, protectedSpans: List<String>, targetRatio: Double): String = "DOC-PRUNED"
|
||||
override suspend fun prune(
|
||||
content: String,
|
||||
protectedSpans: List<String>,
|
||||
targetRatio: Double,
|
||||
): String = "DOC-PRUNED"
|
||||
}
|
||||
val builder = com.correx.core.context.builder.DefaultContextPackBuilder(
|
||||
com.correx.core.context.compression.DefaultContextCompressor(), CompressionPolicy(3), tokenPruner = pruner,
|
||||
@@ -155,8 +159,11 @@ class CompressionPipelineStagesTest {
|
||||
entry("agi", ContextLayer.L0, EntryRole.SYSTEM, bigInstructions).copy(sourceType = "agentInstructions"),
|
||||
)
|
||||
val flat = builder.build(
|
||||
com.correx.core.events.types.ContextPackId("p"), com.correx.core.events.types.SessionId("s"),
|
||||
com.correx.core.events.types.StageId("st"), entries, com.correx.core.context.model.TokenBudget(limit = 4000),
|
||||
com.correx.core.events.types.ContextPackId("p"),
|
||||
com.correx.core.events.types.SessionId("s"),
|
||||
com.correx.core.events.types.StageId("st"),
|
||||
entries,
|
||||
com.correx.core.context.model.TokenBudget(limit = 4000),
|
||||
).layers.values.flatten()
|
||||
assertEquals(bigProfile, flat.first { it.sourceType == "projectProfile" }.content)
|
||||
assertEquals("you are an agent", flat.first { it.sourceType == "systemPrompt" }.content)
|
||||
@@ -171,7 +178,10 @@ class CompressionPipelineStagesTest {
|
||||
assertEquals(ContextClass.STRUCTURED, c.classify(entry("toolLog", ContextLayer.L2, EntryRole.TOOL)))
|
||||
assertEquals(ContextClass.STRUCTURED, c.classify(entry("steeringNote", ContextLayer.L2, EntryRole.SYSTEM)))
|
||||
// Assistant tool-call turns are structured history, never token-pruned (amnesia-loop fix).
|
||||
assertEquals(ContextClass.STRUCTURED, c.classify(entry("assistantToolCall", ContextLayer.L2, EntryRole.ASSISTANT)))
|
||||
assertEquals(
|
||||
ContextClass.STRUCTURED,
|
||||
c.classify(entry("assistantToolCall", ContextLayer.L2, EntryRole.ASSISTANT)),
|
||||
)
|
||||
assertEquals(ContextClass.FREEFORM, c.classify(entry("chat", ContextLayer.L1, EntryRole.USER)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,16 @@ package com.correx.core.approvals
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Serializable
|
||||
enum class Tier(val level: Int) {
|
||||
@SerialName("T0") T0(0),
|
||||
@SerialName("T1") T1(1),
|
||||
@SerialName("T2") T2(2),
|
||||
@SerialName("T3") T3(3),
|
||||
@SerialName("T4") T4(4)
|
||||
enum class Tier {
|
||||
@SerialName("T0") T0,
|
||||
@SerialName("T1") T1,
|
||||
@SerialName("T2") T2,
|
||||
@SerialName("T3") T3,
|
||||
@SerialName("T4") T4;
|
||||
|
||||
/** Escalation level, ordered T0 < T1 < … < T4. Derived from declaration order — no magic numbers. */
|
||||
val level: Int get() = ordinal
|
||||
}
|
||||
|
||||
fun Tier.isAtLeast(other: Tier): Boolean = this.level >= other.level
|
||||
|
||||
+5
-1
@@ -28,6 +28,10 @@ class ArtifactRepairEventSerializationTest {
|
||||
|
||||
@Test
|
||||
fun `ArtifactRepairFailed round-trips`() {
|
||||
roundTrip(ArtifactRepairFailedEvent(ArtifactId("a"), "UNSAFE", "Abort", "path escapes", SessionId("s"), StageId("st")))
|
||||
roundTrip(
|
||||
ArtifactRepairFailedEvent(
|
||||
ArtifactId("a"), "UNSAFE", "Abort", "path escapes", SessionId("s"), StageId("st"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -14,7 +14,11 @@ class PlanLintCompletedEventSerializationTest {
|
||||
sessionId = SessionId("sess-1"),
|
||||
candidateId = "freestyle-sess-1",
|
||||
hardFailures = listOf(
|
||||
PlanLintFinding(code = "unproduced_need", stageId = "implement", detail = "needs 'design', produced by no stage"),
|
||||
PlanLintFinding(
|
||||
code = "unproduced_need",
|
||||
stageId = "implement",
|
||||
detail = "needs 'design', produced by no stage",
|
||||
),
|
||||
PlanLintFinding(code = "trap_state", stageId = "loop", detail = "no path to 'done'"),
|
||||
),
|
||||
softFindings = listOf(
|
||||
|
||||
+5
-1
@@ -44,7 +44,11 @@ class RepoKnowledgeRetrievedEventSerializationTest {
|
||||
query = "context builder",
|
||||
hits = listOf(
|
||||
RepoKnowledgeHit(path = "core/context/ContextBuilder.kt", text = "class ContextBuilder", score = 0.95f),
|
||||
RepoKnowledgeHit(path = "core/context/DefaultContextBuilder.kt", text = "class DefaultContextBuilder", score = 0.88f),
|
||||
RepoKnowledgeHit(
|
||||
path = "core/context/DefaultContextBuilder.kt",
|
||||
text = "class DefaultContextBuilder",
|
||||
score = 0.88f,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertEquals(sample, eventJson.decodeFromString(EventPayload.serializer(),
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@ class RepoMapComputedEventSerializationTest {
|
||||
|
||||
@Test
|
||||
fun `RepoMapComputedEvent without stateKey field decodes with null`() {
|
||||
val json = """{"type":"RepoMapComputed","sessionId":"s","repoRoot":"/repo","entries":[],"computedAt":"2026-06-11T00:00:00Z"}"""
|
||||
val json = """{"type":"RepoMapComputed","sessionId":"s","repoRoot":"/repo",""" +
|
||||
""""entries":[],"computedAt":"2026-06-11T00:00:00Z"}"""
|
||||
val decoded = eventJson.decodeFromString(EventPayload.serializer(), json) as RepoMapComputedEvent
|
||||
assertNull(decoded.stateKey)
|
||||
}
|
||||
|
||||
+6
-1
@@ -23,7 +23,12 @@ class ToolCallAssessedEventSerializationTest {
|
||||
observations = listOf(
|
||||
ToolCallObservation(
|
||||
ruleCode = "PATH_CONTAINMENT",
|
||||
facts = mapOf("path" to "/tmp/x", "exists" to "false", "inWorkspace" to "false", "privileged" to "false"),
|
||||
facts = mapOf(
|
||||
"path" to "/tmp/x",
|
||||
"exists" to "false",
|
||||
"inWorkspace" to "false",
|
||||
"privileged" to "false",
|
||||
),
|
||||
),
|
||||
),
|
||||
disposition = RiskAction.PROMPT_USER,
|
||||
|
||||
@@ -54,7 +54,12 @@ class DecisionJournalRenderer(private val keepLast: Int = 40) {
|
||||
* agents don't act on "Approval APPROVED (tier T2)" or "Advanced x → y", so they're
|
||||
* dropped from the render (still present in [DecisionJournalState.records] for the journal).
|
||||
*/
|
||||
private val AGENT_RELEVANT_KINDS =
|
||||
setOf(DecisionKind.INTENT, DecisionKind.STEERING, DecisionKind.PREEMPT, DecisionKind.FAILURE, DecisionKind.RETRY)
|
||||
private val AGENT_RELEVANT_KINDS = setOf(
|
||||
DecisionKind.INTENT,
|
||||
DecisionKind.STEERING,
|
||||
DecisionKind.PREEMPT,
|
||||
DecisionKind.FAILURE,
|
||||
DecisionKind.RETRY,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -81,12 +81,14 @@ class FileSystemContractEvaluator : ContractAssertionEvaluator {
|
||||
private fun scriptDefined(path: Path, exists: Boolean, name: String): ContractAssertionVerdict {
|
||||
val scripts = jsonObj(path, exists)?.get("scripts")?.let { runCatching { it.jsonObject }.getOrNull() }
|
||||
?: return verdict(false, "no scripts block")
|
||||
return verdict(scripts.containsKey(name), if (scripts.containsKey(name)) "script '$name' defined" else "missing script '$name'")
|
||||
val hasScript = scripts.containsKey(name)
|
||||
return verdict(hasScript, if (hasScript) "script '$name' defined" else "missing script '$name'")
|
||||
}
|
||||
|
||||
private fun contains(path: Path, exists: Boolean, pattern: String): ContractAssertionVerdict {
|
||||
val text = readOrNull(path, exists) ?: return verdict(false, "file does not exist")
|
||||
return verdict(text.contains(pattern), if (text.contains(pattern)) "contains '$pattern'" else "missing '$pattern'")
|
||||
val hasPattern = text.contains(pattern)
|
||||
return verdict(hasPattern, if (hasPattern) "contains '$pattern'" else "missing '$pattern'")
|
||||
}
|
||||
|
||||
private fun textMatch(path: Path, exists: Boolean, needle: String, failMsg: String): ContractAssertionVerdict {
|
||||
|
||||
+3
-1
@@ -158,7 +158,9 @@ class ReplayOrchestrator(
|
||||
session: Session,
|
||||
): ReplayStepResult {
|
||||
log.debug("[Orchestrator] executeStage session=${ctx.sessionId.value} stage=${stageId.value}")
|
||||
return when (val result = executeStage(ctx.sessionId, stageId, ctx.graph, session, ctx.config, effectivesFor(ctx.config))) {
|
||||
val result =
|
||||
executeStage(ctx.sessionId, stageId, ctx.graph, session, ctx.config, effectivesFor(ctx.config))
|
||||
return when (result) {
|
||||
is StageExecutionResult.Success -> ReplayStepResult.Continue(
|
||||
ctx.copy(stageCount = ctx.stageCount + 1),
|
||||
)
|
||||
|
||||
+7
-3
@@ -20,13 +20,17 @@ class ReplayInferenceProvider(
|
||||
|
||||
override val id: ProviderId = ProviderId("replay-provider")
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private companion object {
|
||||
/** Rough char→token ratio for the replay tokenizer (no real BPE at replay time). */
|
||||
const val CHARS_PER_TOKEN = 4
|
||||
}
|
||||
|
||||
override val tokenizer: Tokenizer = object : Tokenizer {
|
||||
override suspend fun tokenize(text: String): List<Token> =
|
||||
List(text.chunked(4).size) { i -> Token(i) } // 1 token ≈ 4 chars
|
||||
List(text.chunked(CHARS_PER_TOKEN).size) { i -> Token(i) }
|
||||
|
||||
override suspend fun countTokens(text: String): Int =
|
||||
(text.length + 3) / 4
|
||||
(text.length + CHARS_PER_TOKEN - 1) / CHARS_PER_TOKEN
|
||||
}
|
||||
|
||||
override suspend fun infer(request: InferenceRequest): InferenceResponse {
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@ import org.junit.jupiter.api.Test
|
||||
|
||||
class ArtifactPolicyDecisionTest {
|
||||
|
||||
private fun decideFresh(f: ArtifactFailure) = decide(f, attemptsSoFar = 0, maxAttempts = Int.MAX_VALUE, hasApprover = false)
|
||||
private fun decideFresh(f: ArtifactFailure) =
|
||||
decide(f, attemptsSoFar = 0, maxAttempts = Int.MAX_VALUE, hasApprover = false)
|
||||
|
||||
@Test
|
||||
fun `unsafe always aborts, even with budget and approver`() {
|
||||
|
||||
+2
-1
@@ -51,7 +51,8 @@ class JournalCompactionServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emits JournalCompactedEvent with correct coversThroughSequence and lowSalienceOmittedCount`(): Unit = runBlocking {
|
||||
fun `emits JournalCompactedEvent with correct coversThroughSequence and lowSalienceOmittedCount`():
|
||||
Unit = runBlocking {
|
||||
val svc = JournalCompactionService(fakeArtifactStore(), { "summary" }, tokenThreshold = { 100 })
|
||||
val state = stateWithRecords(
|
||||
makeRecord(1, DecisionKind.INTENT), // HIGH
|
||||
|
||||
@@ -108,6 +108,9 @@ class DefaultTalkieContextBuilder(
|
||||
|
||||
/** Cap on ideas folded into router context, newest-first, so the board can't balloon L0. */
|
||||
private const val MAX_IDEAS_IN_CONTEXT = 10
|
||||
|
||||
/** Rough chars-per-token heuristic used when no tokenizer is available. */
|
||||
private const val CHARS_PER_TOKEN_ESTIMATE = 4
|
||||
}
|
||||
|
||||
override suspend fun build(
|
||||
@@ -389,7 +392,7 @@ class DefaultTalkieContextBuilder(
|
||||
}
|
||||
|
||||
private fun fallbackEstimate(content: String): Int {
|
||||
return (content.length / 4).coerceAtLeast(1)
|
||||
return (content.length / CHARS_PER_TOKEN_ESTIMATE).coerceAtLeast(1)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -146,7 +146,12 @@ class DefaultTalkieFacade(
|
||||
// Rebuild state so lastRetrievedMemory reflects this turn
|
||||
state = routerRepository.getTalkieState(sessionId)
|
||||
|
||||
val contextPack = routerContextBuilder.build(state, config.tokenBudget, workflowSummaryProvider(), sessionProfileProvider(sessionId))
|
||||
val contextPack = routerContextBuilder.build(
|
||||
state,
|
||||
config.tokenBudget,
|
||||
workflowSummaryProvider(),
|
||||
sessionProfileProvider(sessionId),
|
||||
)
|
||||
|
||||
if (contextPack.compressionMetadata.entriesDropped > 0) {
|
||||
val truncNow = Clock.System.now()
|
||||
@@ -195,7 +200,8 @@ class DefaultTalkieFacade(
|
||||
"without producing an answer (likely spent on hidden reasoning). " +
|
||||
"Raise [router.generation] max_tokens or lower the model's reasoning budget, then ask again."
|
||||
} else {
|
||||
"The model returned an empty response (finish reason: ${inferenceResponse.finishReason::class.simpleName}). Try again."
|
||||
"The model returned an empty response " +
|
||||
"(finish reason: ${inferenceResponse.finishReason::class.simpleName}). Try again."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,13 +33,22 @@ data class TaskContextBundle(
|
||||
appendLine("task $id [$status] ${title.orEmpty()}".trimEnd())
|
||||
goal?.let { appendLine("goal: $it") }
|
||||
if (blocked) appendLine("BLOCKED — waiting on ${blockedBy.size} unfinished dependency(ies):")
|
||||
appendList("blocked_by", blockedBy) { " - ${it.id} [${it.status}] ${it.title.orEmpty()} (${it.link})".trimEnd() }
|
||||
appendList("blocked_by", blockedBy) {
|
||||
" - ${it.id} [${it.status}] ${it.title.orEmpty()} (${it.link})".trimEnd()
|
||||
}
|
||||
appendList("acceptance_criteria", acceptanceCriteria) { " - $it" }
|
||||
if (relevantFiles.isNotEmpty()) appendLine("relevant_files: ${relevantFiles.joinToString(", ")}")
|
||||
appendList("dependencies", dependencies) { " - ${it.id} [${it.status}] ${it.title.orEmpty()} (${it.link})".trimEnd() }
|
||||
appendList("dependencies", dependencies) {
|
||||
" - ${it.id} [${it.status}] ${it.title.orEmpty()} (${it.link})".trimEnd()
|
||||
}
|
||||
appendList("documents", documents) { " - ${it.targetId} (${it.type}) ${it.title} [${it.path}]: ${it.excerpt}" }
|
||||
appendList("artifacts", artifacts) { " - ${it.targetId} (${it.type})${it.stage?.let { s -> " @$s" }.orEmpty()}: ${it.excerpt.orEmpty()}".trimEnd() }
|
||||
appendList("sessions", sessions) { " - ${it.targetId} (${it.type}) [${it.status}] ${it.intent.orEmpty()}".trimEnd() }
|
||||
appendList("artifacts", artifacts) {
|
||||
val stageSuffix = it.stage?.let { s -> " @$s" }.orEmpty()
|
||||
" - ${it.targetId} (${it.type})$stageSuffix: ${it.excerpt.orEmpty()}".trimEnd()
|
||||
}
|
||||
appendList("sessions", sessions) {
|
||||
" - ${it.targetId} (${it.type}) [${it.status}] ${it.intent.orEmpty()}".trimEnd()
|
||||
}
|
||||
appendList("related", relatedLinks) { " - ${it.targetId} (${it.type})" }
|
||||
appendList("relevant_knowledge", relevantKnowledge) { " - ${it.source}: ${it.text}" }
|
||||
appendList("notes", notes) { " - [${it.author}] ${it.body}" }
|
||||
|
||||
@@ -118,7 +118,12 @@ class DefaultTaskReducerTest {
|
||||
val state = reduceAll(
|
||||
created(),
|
||||
TaskAcceptanceCriteriaSetEvent(taskId, listOf("refresh endpoint exists")),
|
||||
TaskLinkedEvent(taskId, targetId = "auth-138", type = TaskLinkType.DEPENDS_ON, targetKind = TaskTargetKind.TASK),
|
||||
TaskLinkedEvent(
|
||||
taskId,
|
||||
targetId = "auth-138",
|
||||
type = TaskLinkType.DEPENDS_ON,
|
||||
targetKind = TaskTargetKind.TASK,
|
||||
),
|
||||
TaskNoteAddedEvent(taskId, author = TaskNoteAuthor.AGENT, body = "migration failed"),
|
||||
)
|
||||
|
||||
|
||||
+2
-1
@@ -43,7 +43,8 @@ class ManifestContainmentRule : ToolCallRule {
|
||||
|
||||
for (raw in candidatePathStrings(input.paramRoles, input.request.parameters)) {
|
||||
val candidate = Path.of(raw)
|
||||
val resolvedInput = if (candidate.isAbsolute) candidate else input.workspace.workspaceRoot.resolve(candidate)
|
||||
val resolvedInput =
|
||||
if (candidate.isAbsolute) candidate else input.workspace.workspaceRoot.resolve(candidate)
|
||||
val resolvedReal = input.probe.resolveReal(resolvedInput)
|
||||
val inWorkspace = resolvedReal.startsWith(workspaceReal)
|
||||
val relative = if (inWorkspace) workspaceReal.relativize(resolvedReal).toString() else raw
|
||||
|
||||
+2
-1
@@ -35,7 +35,8 @@ class PathContainmentRule : ToolCallRule {
|
||||
|
||||
for (raw in candidatePathStrings(input.paramRoles, input.request.parameters)) {
|
||||
val candidate = Path.of(raw)
|
||||
val resolvedInput = if (candidate.isAbsolute) candidate else input.workspace.workspaceRoot.resolve(candidate)
|
||||
val resolvedInput =
|
||||
if (candidate.isAbsolute) candidate else input.workspace.workspaceRoot.resolve(candidate)
|
||||
val resolvedReal = input.probe.resolveReal(resolvedInput)
|
||||
val exists = input.probe.exists(resolvedInput)
|
||||
val privileged = privilegedReal.any { resolvedReal.startsWith(it) }
|
||||
|
||||
+7
-1
@@ -26,7 +26,13 @@ class ReferenceExistsRuleTest {
|
||||
private fun abs(p: String): Path = Path.of(p).toAbsolutePath().normalize()
|
||||
|
||||
private fun input(pathArg: String, probe: WorldProbe) = ToolCallAssessmentInput(
|
||||
request = ToolRequest(ToolInvocationId("i"), SessionId("s"), StageId("st"), "file_read", mapOf("path" to pathArg)),
|
||||
request = ToolRequest(
|
||||
ToolInvocationId("i"),
|
||||
SessionId("s"),
|
||||
StageId("st"),
|
||||
"file_read",
|
||||
mapOf("path" to pathArg),
|
||||
),
|
||||
capabilities = setOf(ToolCapability.FILE_READ),
|
||||
workspace = WorkspacePolicy(workspace, emptyList()),
|
||||
probe = probe,
|
||||
|
||||
@@ -27,7 +27,13 @@ class StaleWriteRuleTest {
|
||||
private fun abs(p: String): Path = Path.of(p).toAbsolutePath().normalize()
|
||||
|
||||
private fun input(onDisk: String, session: SessionContext) = ToolCallAssessmentInput(
|
||||
request = ToolRequest(ToolInvocationId("i"), SessionId("s"), StageId("st"), "file_edit", mapOf("path" to target)),
|
||||
request = ToolRequest(
|
||||
ToolInvocationId("i"),
|
||||
SessionId("s"),
|
||||
StageId("st"),
|
||||
"file_edit",
|
||||
mapOf("path" to target),
|
||||
),
|
||||
capabilities = setOf(ToolCapability.FILE_WRITE),
|
||||
workspace = WorkspacePolicy(workspace, emptyList()),
|
||||
probe = FakeProbe(mapOf(abs(target) to onDisk)),
|
||||
|
||||
@@ -23,7 +23,13 @@ class WriteScopeRuleTest {
|
||||
}
|
||||
|
||||
private fun input(pathArg: String, active: ActiveTask?) = ToolCallAssessmentInput(
|
||||
request = ToolRequest(ToolInvocationId("i"), SessionId("s"), StageId("st"), "file_write", mapOf("path" to pathArg)),
|
||||
request = ToolRequest(
|
||||
ToolInvocationId("i"),
|
||||
SessionId("s"),
|
||||
StageId("st"),
|
||||
"file_write",
|
||||
mapOf("path" to pathArg),
|
||||
),
|
||||
capabilities = setOf(ToolCapability.FILE_WRITE),
|
||||
workspace = WorkspacePolicy(workspace, emptyList()),
|
||||
probe = FakeProbe(),
|
||||
@@ -34,7 +40,10 @@ class WriteScopeRuleTest {
|
||||
|
||||
@Test
|
||||
fun `no active task means nothing to enforce`() {
|
||||
assertEquals(RiskAction.PROCEED, rule.assess(input("/work/project/core/billing/X.kt", active = null)).disposition)
|
||||
assertEquals(
|
||||
RiskAction.PROCEED,
|
||||
rule.assess(input("/work/project/core/billing/X.kt", active = null)).disposition,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+4
-1
@@ -20,7 +20,10 @@ class ArtifactExtractionPipelineTest {
|
||||
)
|
||||
|
||||
private fun resolved(raw: String) =
|
||||
assertInstanceOf(ArtifactExtractionPipeline.ExtractionResult.Resolved::class.java, pipeline.run(raw, fileWritten))
|
||||
assertInstanceOf(
|
||||
ArtifactExtractionPipeline.ExtractionResult.Resolved::class.java,
|
||||
pipeline.run(raw, fileWritten),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `clean json passes through`() {
|
||||
|
||||
+4
-1
@@ -45,7 +45,10 @@ class JsonSchemaValidatorTest {
|
||||
@Test
|
||||
fun `unknown property is allowed when additionalProperties is true`() {
|
||||
val open = schema.copy(additionalProperties = true)
|
||||
val result = JsonSchemaValidator.validate(open, Json.parseToJsonElement("""{"summary":"ok","score":3,"extra":1}"""))
|
||||
val result = JsonSchemaValidator.validate(
|
||||
open,
|
||||
Json.parseToJsonElement("""{"summary":"ok","score":3,"extra":1}"""),
|
||||
)
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user