feat(validation): staged-verification funnel — contract + execution gates

Gate 2 (contract) and Gate 4 (execution) of the 4-gate staged-verification
design, plus the two gate-quality fixes found in live QA.

- Gate 2: KindContractTable (kind→assertions registry, universal floor,
  additive project overrides) + KindInference (path→kind, pure/replay-safe)
  + ContractAssertion model; ContractGateEvaluatedEvent (registered);
  ContractAssertionEvaluator seam + FileSystemContractEvaluator (FS/TEXT,
  COMPILER skipped); runContractGate in runPostStageGates — a stage that
  produces file_written artifacts must have every declared+written file
  exist/non-empty/parse, else retryable hand-back with per-assertion feedback.
- Option A: ExecutionPlanCompiler puts concrete (non-glob) plan `writes` into
  StageConfig.expectedFiles, so a declared-but-unwritten file fails file_exists.
- Gate 4: BuildExpectation enum (none|module|project|tests) + plan field +
  closed-set compiler validation; runExecutionGate resolves the alias against
  the bound project profile commands and runs it as a deterministic gate.
- Live-QA fixes: react_entry kind so main.tsx/index.tsx aren't required to
  export a default component (was an unwinnable false-positive); JSONC
  tolerance (allowComments/allowTrailingComma) so tsconfig.json passes valid_json.

Seams are null on the replay harness (no-op) — recorded events are truth (#9).
This commit is contained in:
2026-07-06 01:25:51 +04:00
parent 33ea44d8cc
commit ff1a0ffdad
18 changed files with 935 additions and 5 deletions
@@ -0,0 +1,32 @@
package com.correx.core.transitions.graph
/**
* Per-stage execution-gate scope (staged-verification design §2) — the closed set a planner picks
* from to say how far the project should be runnable after this stage. Resolves at runtime to a
* concrete command drawn from the bound project profile's command aliases, so the command itself
* stays stack-specific and operator-owned while the *scope* is a fixed vocabulary the planner can't
* invent.
*
* [commandAlias] names the `[project.commands]` alias to run for this scope (null = no gate):
* NONE → nothing (early stages where the project is not yet runnable), MODULE → typecheck,
* PROJECT → build, TESTS → test. A stage whose profile lacks the alias skips the gate with a warning
* rather than failing — the vocabulary degrades safely when the operator hasn't configured commands.
*/
enum class BuildExpectation(val commandAlias: String?) {
NONE(null),
MODULE("typecheck"),
PROJECT("build"),
TESTS("test"),
;
companion object {
/** Parses a plan's `build_expectation` string; null for an unrecognized value so the compiler rejects it. */
fun fromPlan(raw: String?): BuildExpectation? = when (raw?.trim()?.lowercase()) {
null, "", "none" -> NONE
"module" -> MODULE
"project" -> PROJECT
"tests", "test" -> TESTS
else -> null
}
}
}
@@ -28,5 +28,15 @@ data class StageConfig(
// A non-clean command fails the stage retryably with its output fed back, so only
// static-clean output reaches the downstream reviewer. Empty = no static-first step.
val staticAnalysis: List<String> = emptyList(),
// Execution-gate scope for this stage (staged-verification §2). NONE = no build gate (default,
// e.g. early scaffold/types stages); MODULE/PROJECT/TESTS resolve to the bound project profile's
// typecheck/build/test command and run it as a deterministic Gate 4 after the stage produces.
val buildExpectation: BuildExpectation = BuildExpectation.NONE,
// Concrete (non-glob) workspace-relative paths this stage declares it will produce (Gate 2
// Option A, staged-verification §3.2). The contract gate checks each declared file exists and
// satisfies its inferred-kind contract, so a file the plan promised but the stage never wrote
// FAILS the gate — catching missing files, which the runtime write-manifest alone (it only
// inspects files that WERE written) cannot. Derived from the concrete entries of PlanStage.writes.
val expectedFiles: List<String> = emptyList(),
val metadata: Map<String, String> = emptyMap(),
)