Skip to content

Commit 51156ca

Browse files
committed
Keep active validators from being downgraded
1 parent 5fc0b7c commit 51156ca

3 files changed

Lines changed: 153 additions & 33 deletions

File tree

internal/app/finish_gate.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,27 @@ func activeValidatorValidationCovers(capability activeCapability, evidenceText s
135135
if command == "" {
136136
return false
137137
}
138-
validation := normalizeSentence(firstUsefulValidationMemory(sectionBody(evidenceText, "Validation")))
139-
if !credibleActiveCapabilityEvidence(validation) {
138+
validation := normalizeSentence(sectionBody(evidenceText, "Validation"))
139+
if !strings.Contains(validation, command) || !credibleActiveCapabilityEvidence(validation) {
140140
return false
141141
}
142-
return strings.Contains(validation, command)
142+
return successfulValidationEvidence(validation)
143+
}
144+
145+
func successfulValidationEvidence(normalized string) bool {
146+
return hasAny(normalized,
147+
"passed",
148+
"success",
149+
"succeeded",
150+
"verified",
151+
"checked",
152+
"covered",
153+
"proved",
154+
"proven",
155+
"built",
156+
" ok ",
157+
"ok ./",
158+
)
143159
}
144160

145161
func credibleActiveCapabilityEvidence(normalized string) bool {
@@ -149,6 +165,10 @@ func credibleActiveCapabilityEvidence(normalized string) bool {
149165
if explicitActiveCapabilityBlocker(normalized) {
150166
return true
151167
}
168+
if hasAny(normalized, "failed", "failure", "blocked", "warning", "warn") &&
169+
!hasAny(normalized, "passed", "success", "succeeded", "verified", "checked", "covered", "handled", "proved", "proven", "recovered") {
170+
return false
171+
}
152172
if hasAny(normalized,
153173
"pending",
154174
"todo",

internal/app/growth.go

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,7 @@ func growthLine(verb string, pressure growthPressure, label string) string {
756756

757757
func materializeGrowthCandidates(root string, pressures []growthPressure, previous growthState) ([]growthCandidate, *hyperError) {
758758
candidates := []growthCandidate{}
759-
seen := map[string]bool{}
759+
seen := map[string]int{}
760760
for _, pressure := range pressures {
761761
if pressure.GoalCount < growthRepeatedSignalGoals {
762762
continue
@@ -768,44 +768,24 @@ func materializeGrowthCandidates(root string, pressures []growthPressure, previo
768768
if pressure.PressureType == "surface_validation" {
769769
reason = "Repeated surface proof pressure crossed the validator threshold."
770770
}
771-
candidate := growthCandidateForPressure("validator", prefix, "validators", reason, pressure)
772-
if err := writeGrowthCandidate(root, candidate, pressure); err != nil {
771+
if err := addGrowthCandidate(root, &candidates, seen, growthCandidateForPressure("validator", prefix, "validators", reason, pressure), pressure); err != nil {
773772
return nil, err
774773
}
775-
if !seen[candidate.LifecyclePath] {
776-
candidates = append(candidates, candidate)
777-
seen[candidate.LifecyclePath] = true
778-
}
779774
case "implementation":
780-
candidate := growthCandidateForPressure("skill", "skill", "skills", "Repeated implementation pressure crossed the skill threshold.", pressure)
781-
if err := writeGrowthCandidate(root, candidate, pressure); err != nil {
775+
if err := addGrowthCandidate(root, &candidates, seen, growthCandidateForPressure("skill", "skill", "skills", "Repeated implementation pressure crossed the skill threshold.", pressure), pressure); err != nil {
782776
return nil, err
783777
}
784-
if !seen[candidate.LifecyclePath] {
785-
candidates = append(candidates, candidate)
786-
seen[candidate.LifecyclePath] = true
787-
}
788778
case "stop_condition":
789-
candidate := growthCandidateForPressure("validator", "preflight", "validators", "Repeated failure pressure crossed the preflight threshold.", pressure)
790-
if err := writeGrowthCandidate(root, candidate, pressure); err != nil {
779+
if err := addGrowthCandidate(root, &candidates, seen, growthCandidateForPressure("validator", "preflight", "validators", "Repeated failure pressure crossed the preflight threshold.", pressure), pressure); err != nil {
791780
return nil, err
792781
}
793-
if !seen[candidate.LifecyclePath] {
794-
candidates = append(candidates, candidate)
795-
seen[candidate.LifecyclePath] = true
796-
}
797782
}
798783
}
799784
if harnessPressureReady(pressures) {
800785
pressure := aggregateHarnessPressure(pressures)
801-
candidate := harnessCandidateForPressure(pressure)
802-
if err := writeGrowthCandidate(root, candidate, pressure); err != nil {
786+
if err := addGrowthCandidate(root, &candidates, seen, harnessCandidateForPressure(pressure), pressure); err != nil {
803787
return nil, err
804788
}
805-
if !seen[candidate.LifecyclePath] {
806-
candidates = append(candidates, candidate)
807-
seen[candidate.LifecyclePath] = true
808-
}
809789
}
810790
active, activeErr := activeCapabilities(root)
811791
if activeErr != nil {
@@ -820,6 +800,51 @@ func materializeGrowthCandidates(root string, pressures []growthPressure, previo
820800
return candidates, nil
821801
}
822802

803+
func addGrowthCandidate(root string, candidates *[]growthCandidate, seen map[string]int, candidate growthCandidate, pressure growthPressure) *hyperError {
804+
key := growthCandidateIdentity(candidate)
805+
if index, ok := seen[key]; ok {
806+
existing := (*candidates)[index]
807+
if strongerOrEqualGrowthCandidate(existing, candidate) {
808+
return nil
809+
}
810+
if err := writeGrowthCandidate(root, candidate, pressure); err != nil {
811+
return err
812+
}
813+
(*candidates)[index] = candidate
814+
return nil
815+
}
816+
if err := writeGrowthCandidate(root, candidate, pressure); err != nil {
817+
return err
818+
}
819+
seen[key] = len(*candidates)
820+
*candidates = append(*candidates, candidate)
821+
return nil
822+
}
823+
824+
func strongerOrEqualGrowthCandidate(existing, candidate growthCandidate) bool {
825+
existingRank := growthCandidateStatusRank(existing.Status)
826+
candidateRank := growthCandidateStatusRank(candidate.Status)
827+
if existingRank != candidateRank {
828+
return existingRank > candidateRank
829+
}
830+
return existing.EvidenceCount >= candidate.EvidenceCount
831+
}
832+
833+
func growthCandidateStatusRank(status string) int {
834+
switch status {
835+
case "active":
836+
return 4
837+
case "promotable":
838+
return 3
839+
case "repeated":
840+
return 2
841+
case "observed", "candidate":
842+
return 1
843+
default:
844+
return 0
845+
}
846+
}
847+
823848
func validatorCandidatePrefix(pressure growthPressure) string {
824849
if pressure.PressureType != "surface_validation" {
825850
return "validator"
@@ -998,22 +1023,26 @@ func retiredGrowthCandidates(root string, previous growthState, current []growth
9981023
func harnessPressureReady(pressures []growthPressure) bool {
9991024
stable := 0
10001025
hasValidation := false
1001-
nonValidationStructures := 0
1026+
hasImplementation := false
1027+
hasWorkBoundary := false
10021028
for _, pressure := range pressures {
10031029
if pressure.GoalCount < growthRepeatedSignalGoals {
10041030
continue
10051031
}
10061032
if pressure.Effect == "validation" {
10071033
hasValidation = true
10081034
}
1009-
if pressure.Effect == "implementation" || pressure.Effect == "work_boundary" {
1010-
nonValidationStructures++
1035+
if pressure.Effect == "implementation" {
1036+
hasImplementation = true
1037+
}
1038+
if pressure.Effect == "work_boundary" {
1039+
hasWorkBoundary = true
10111040
}
10121041
if pressure.Effect == "validation" || pressure.Effect == "implementation" || pressure.Effect == "work_boundary" {
10131042
stable++
10141043
}
10151044
}
1016-
return hasValidation && nonValidationStructures >= 2 && stable >= growthHarnessStablePressures
1045+
return hasValidation && hasImplementation && hasWorkBoundary && stable >= growthHarnessStablePressures
10171046
}
10181047

10191048
func aggregateHarnessPressure(pressures []growthPressure) growthPressure {

internal/app/main_test.go

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ func TestCompleteAcceptsValidationOutputForActiveValidator(t *testing.T) {
729729
mustInitWithPlan(t, root, "Tiny CLI", "Build a tiny CLI MVP")
730730
mustRun(t, root, "run")
731731
writeFile(t, filepath.Join(root, ".hyper", "capabilities", "active", "validator", "validator-go-test.md"), "# validator-go-test\n\nStatus: active\nKind: validator\nSignal: Run go test ./... before completing packets.\n")
732-
writeFile(t, filepath.Join(root, ".hyper", "goals", "GOAL-0001", "evidence.md"), "# GOAL-0001 Evidence\n\n## Validation\n\nCommand: `go test ./...`\n\nOutput:\n\n```text\nok ./...\n```\n\ngo test ./... passed.\n\n## Readiness Evidence\n\nCore UX: CLI smoke verified create and complete flow.\nValidation coverage: `go test ./...` passed and primary CLI smoke is repeatable.\n\n## Active Capability Evidence\n\nvalidator-go-test: Pending. Required behavior: Run go test ./... before completing packets.\n\n## Blocker\n\nNone blocking.\n")
732+
writeFile(t, filepath.Join(root, ".hyper", "goals", "GOAL-0001", "evidence.md"), "# GOAL-0001 Evidence\n\n## Validation\n\nCommand: `go test ./...`\n\nOutput:\n\n```text\nok ./...\n```\n\n## Readiness Evidence\n\nCore UX: CLI smoke verified create and complete flow.\nValidation coverage: `go test ./...` passed and primary CLI smoke is repeatable.\n\n## Active Capability Evidence\n\nvalidator-go-test: Pending. Required behavior: Run go test ./... before completing packets.\n\n## Blocker\n\nNone blocking.\n")
733733
writeFile(t, filepath.Join(root, ".hyper", "goals", "GOAL-0001", "next.md"), "# GOAL-0001 Next\n\n## Recommended Next Goal\n\nReview stage advancement.\n")
734734

735735
out, err := runCLI(args("complete"), testRoot(root), fakeUpdater{})
@@ -739,6 +739,21 @@ func TestCompleteAcceptsValidationOutputForActiveValidator(t *testing.T) {
739739
assertContains(t, out.Stdout, "Finish gate: passed")
740740
}
741741

742+
func TestCompleteRejectsFailedValidationOutputForActiveValidator(t *testing.T) {
743+
root := t.TempDir()
744+
mustInitWithPlan(t, root, "Tiny CLI", "Build a tiny CLI MVP")
745+
mustRun(t, root, "run")
746+
writeFile(t, filepath.Join(root, ".hyper", "capabilities", "active", "validator", "validator-go-test.md"), "# validator-go-test\n\nStatus: active\nKind: validator\nSignal: Run go test ./... before completing packets.\n")
747+
writeFile(t, filepath.Join(root, ".hyper", "goals", "GOAL-0001", "evidence.md"), "# GOAL-0001 Evidence\n\n## Validation\n\nCommand: `go test ./...`\n\nOutput:\n\n```text\nFAIL ./...\n```\n\ngo test ./... failed.\n\n## Readiness Evidence\n\nCore UX: CLI smoke verified create and complete flow.\nValidation coverage: `go test ./...` failed and needs repair.\n\n## Active Capability Evidence\n\nvalidator-go-test: Pending. Required behavior: Run go test ./... before completing packets.\n\n## Blocker\n\nNone blocking.\n")
748+
writeFile(t, filepath.Join(root, ".hyper", "goals", "GOAL-0001", "next.md"), "# GOAL-0001 Next\n\n## Recommended Next Goal\n\nRepair the failing validation.\n")
749+
750+
_, err := runCLI(args("complete"), testRoot(root), fakeUpdater{})
751+
if err == nil {
752+
t.Fatal("failed validator output must not satisfy active validator proof")
753+
}
754+
assertContains(t, err.Message, "Record active capability evidence for: validator-go-test")
755+
}
756+
742757
func TestCompleteAcceptsExplicitActiveCapabilityBlocker(t *testing.T) {
743758
root := t.TempDir()
744759
mustInitWithPlan(t, root, "Tiny CLI", "Build a tiny CLI MVP")
@@ -1600,6 +1615,62 @@ func TestHarnessCandidateNeedsMultipleNonValidationStructures(t *testing.T) {
16001615
}
16011616
}
16021617

1618+
func TestHarnessCandidateNeedsImplementationAndBoundaryPressure(t *testing.T) {
1619+
pressures := []growthPressure{
1620+
{Effect: "validation", GoalCount: 4, Sources: []string{"GOAL-0001", "GOAL-0002", "GOAL-0003", "GOAL-0004"}},
1621+
{Effect: "work_boundary", GoalCount: 4, Sources: []string{"GOAL-0001", "GOAL-0002", "GOAL-0003", "GOAL-0004"}},
1622+
{Effect: "work_boundary", GoalCount: 4, Sources: []string{"GOAL-0001", "GOAL-0002", "GOAL-0003", "GOAL-0004"}},
1623+
}
1624+
if harnessPressureReady(pressures) {
1625+
t.Fatal("repeated decisions plus validation must not create a harness without implementation pressure")
1626+
}
1627+
}
1628+
1629+
func TestDuplicateCommandCandidatesKeepStrongestLifecycle(t *testing.T) {
1630+
root := t.TempDir()
1631+
if err := ensureProjectLayout(root); err != nil {
1632+
t.Fatalf("layout failed: %v", err)
1633+
}
1634+
pressures := []growthPressure{
1635+
{
1636+
Kind: "pattern",
1637+
PressureType: "repeated_validation",
1638+
Signal: "validation pattern: `./check.sh` passed.",
1639+
Effect: "validation",
1640+
State: "repeated",
1641+
GoalCount: 4,
1642+
MemoryCount: 4,
1643+
Sources: []string{"GOAL-0001", "GOAL-0002", "GOAL-0003", "GOAL-0004"},
1644+
},
1645+
{
1646+
Kind: "pattern",
1647+
PressureType: "repeated_validation",
1648+
Signal: "`./check.sh` passed as active validator smoke.",
1649+
Effect: "validation",
1650+
State: "repeated",
1651+
GoalCount: 2,
1652+
MemoryCount: 2,
1653+
Sources: []string{"GOAL-0005", "GOAL-0006"},
1654+
},
1655+
}
1656+
candidates, hyperErr := materializeGrowthCandidates(root, pressures, growthState{})
1657+
if hyperErr != nil {
1658+
t.Fatalf("materialize candidates failed: %v", hyperErr)
1659+
}
1660+
if len(candidates) != 1 {
1661+
t.Fatalf("expected one deduped validator candidate, got %+v", candidates)
1662+
}
1663+
if candidates[0].Status != "active" {
1664+
t.Fatalf("expected strongest active validator to win, got %+v", candidates[0])
1665+
}
1666+
if !exists(filepath.Join(root, ".hyper", "capabilities", "active", "validator", "validator-check-sh.md")) {
1667+
t.Fatal("active validator file should exist")
1668+
}
1669+
if exists(filepath.Join(root, ".hyper", "capabilities", "candidates", "validator", "validator-check-sh.md")) {
1670+
t.Fatal("weaker duplicate validator candidate should not overwrite active validator")
1671+
}
1672+
}
1673+
16031674
func TestHarnessCandidateRequiresEnoughSourceGoalsForActivation(t *testing.T) {
16041675
twoGoalPressure := aggregateHarnessPressure([]growthPressure{
16051676
{Effect: "validation", GoalCount: 2, Sources: []string{"GOAL-0003", "GOAL-0004"}},

0 commit comments

Comments
 (0)