feat(context): reasoning-thread continuity, L3 doc filtering, and gate hardening
Threads reasoning_content across inference calls and journal renders, filters markdown noise out of L3 repo-knowledge retrieval, adds PlanLinter H3 checks, and tightens filesystem tool output/dir-listing behavior surfaced by prior live QA (see project_readloop_campaign memory).
This commit is contained in:
+30
-1
@@ -41,7 +41,7 @@ object PlanLinter {
|
||||
|
||||
fun lint(graph: WorkflowGraph, seeds: Set<String> = seedArtifacts): PlanLintResult =
|
||||
PlanLintResult(
|
||||
hardFailures = unproducedNeeds(graph, seeds) + trapStates(graph),
|
||||
hardFailures = unproducedNeeds(graph, seeds) + trapStates(graph) + unreferencedPromptArtifacts(graph, seeds),
|
||||
softFindings = stageCount(graph) + fanOut(graph) + emptyBriefs(graph) + duplicateBriefs(graph),
|
||||
)
|
||||
|
||||
@@ -89,6 +89,35 @@ object PlanLinter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* H3: a stage's inline prompt mentions an artifact ID that is produced by the plan or
|
||||
* provided by a seed, but the stage does not declare it in its `needs`. The stage will
|
||||
* not have access to that artifact at runtime, so any reference in the prompt is a dangling
|
||||
* assumption that will confuse or stall the model — force the architect to either add the
|
||||
* artifact to `needs` or reword the prompt.
|
||||
*/
|
||||
private fun unreferencedPromptArtifacts(graph: WorkflowGraph, seeds: Set<String>): List<PlanLintFinding> {
|
||||
val produced = graph.stages.values.flatMap { it.produces }.map { it.name.value }.toSet() + seeds
|
||||
return graph.stages.entries.flatMap { (id, stage) ->
|
||||
val prompt = briefOf(stage)
|
||||
val needIds = stage.needs.map { it.value }.toSet()
|
||||
val ownProduces = stage.produces.map { it.name.value }.toSet()
|
||||
produced.filter { prodId ->
|
||||
prompt.isNotBlank() &&
|
||||
prodId.length >= 2 && // skip single-char IDs (false-positive risk with substring matching)
|
||||
Regex("\\b${Regex.escape(prodId)}\\b").containsMatchIn(prompt) &&
|
||||
prodId !in needIds &&
|
||||
prodId !in ownProduces
|
||||
}.map { prodId ->
|
||||
PlanLintFinding(
|
||||
code = "unreferenced_prompt_artifact",
|
||||
stageId = id.value,
|
||||
detail = "prompt mentions '$prodId' but it is not declared in needs",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** S1: more stages than the ceiling. */
|
||||
private fun stageCount(graph: WorkflowGraph): List<PlanLintFinding> =
|
||||
if (graph.stages.size > STAGE_CEILING) {
|
||||
|
||||
+31
@@ -92,6 +92,37 @@ class PlanLinterTest {
|
||||
assertTrue(result.passed, "analysis is a planning-phase seed, not an unproduced need")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a stage mentioning its own produced artifact in the prompt is not a failure`() {
|
||||
val g = graph(
|
||||
stages = mapOf(
|
||||
"a" to stage(produces = listOf("plan"), prompt = "write the plan artifact"),
|
||||
"b" to stage(needs = listOf("plan"), prompt = "build"),
|
||||
),
|
||||
edges = listOf("a" to "b", "b" to "done"),
|
||||
start = "a",
|
||||
)
|
||||
val result = PlanLinter.lint(g)
|
||||
assertTrue(result.passed, "expected pass, hard=${result.hardFailures}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a prompt mentioning another stage's artifact not in needs is a hard failure`() {
|
||||
val g = graph(
|
||||
stages = mapOf(
|
||||
"a" to stage(produces = listOf("plan"), prompt = "write the plan artifact"),
|
||||
"b" to stage(produces = listOf("report"), prompt = "use plan to build"),
|
||||
),
|
||||
edges = listOf("a" to "b", "b" to "done"),
|
||||
start = "a",
|
||||
)
|
||||
val result = PlanLinter.lint(g)
|
||||
assertFalse(result.passed)
|
||||
val h = result.hardFailures.single { it.code == "unreferenced_prompt_artifact" }
|
||||
assertEquals("b", h.stageId)
|
||||
assertTrue(h.detail.contains("plan"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a cycle with no exit is a trap-state hard failure`() {
|
||||
val g = graph(
|
||||
|
||||
Reference in New Issue
Block a user