fix: correct segment expression formatting and rule detection

- Fix formatSegmentExpression to show "5 + 3" for direct 6-9 adds instead of "(5 - 3)"
- Fix determineSegmentDecisions to only classify complement rules when negative terms exist
- Prevent mislabeling direct 6-9 additions as FiveComplement rules
- Add comprehensive test suite validating expression format and rule classification
- Direct additions now properly show addition expressions without parentheses
- Complement operations maintain parentheses format: "(pos - neg1 - neg2)"

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-25 16:01:56 -05:00
parent 2c095162e8
commit e60f4384c3
2 changed files with 139 additions and 14 deletions

View File

@@ -0,0 +1,110 @@
import { describe, it, expect } from 'vitest'
import { generateUnifiedInstructionSequence } from '../unifiedStepGenerator'
describe('segment expression & rule detection', () => {
it('direct 8 at ones: expression is "5 + 3", rule is Direct', () => {
const seq = generateUnifiedInstructionSequence(0, 8) // +8
const seg = seq.segments.find(s => s.place === 0)!
expect(seg.expression.replace(/\s+/g, '')).toBe('5+3')
expect(seg.plan.some(p => p.rule === 'Direct')).toBe(true)
expect(seg.plan.some(p => p.rule === 'FiveComplement')).toBe(false)
expect(seg.plan.some(p => p.rule === 'TenComplement')).toBe(false)
})
it('direct 6 at ones: expression is "5 + 1", rule is Direct', () => {
const seq = generateUnifiedInstructionSequence(0, 6) // +6
const seg = seq.segments.find(s => s.place === 0)!
expect(seg.expression.replace(/\s+/g, '')).toBe('5+1')
expect(seg.plan.some(p => p.rule === 'Direct')).toBe(true)
expect(seg.plan.some(p => p.rule === 'FiveComplement')).toBe(false)
})
it('direct 9 at ones: expression is "5 + 4", rule is Direct', () => {
const seq = generateUnifiedInstructionSequence(0, 9) // +9
const seg = seq.segments.find(s => s.place === 0)!
expect(seg.expression.replace(/\s+/g, '')).toBe('5+4')
expect(seg.plan.some(p => p.rule === 'Direct')).toBe(true)
expect(seg.plan.some(p => p.rule === 'FiveComplement')).toBe(false)
})
it('five-complement at ones (start 2, +3): expression is "(5 - 2)"', () => {
const seq = generateUnifiedInstructionSequence(2, 5)
const seg = seq.segments.find(s => s.place === 0)!
expect(seg.expression.replace(/\s+/g, '')).toBe('(5-2)')
expect(seg.plan.some(p => p.rule === 'FiveComplement')).toBe(true)
})
it('five-complement at ones (start 1, +4): expression is "(5 - 1)"', () => {
const seq = generateUnifiedInstructionSequence(1, 5)
const seg = seq.segments.find(s => s.place === 0)!
expect(seg.expression.replace(/\s+/g, '')).toBe('(5-1)')
expect(seg.plan.some(p => p.rule === 'FiveComplement')).toBe(true)
})
it('ten-complement no cascade (19 + 1 = 20): has TenComplement but not Cascade', () => {
const seq = generateUnifiedInstructionSequence(19, 20)
const seg = seq.segments.find(s => s.place === 0)!
expect(seg.plan.some(p => p.rule === 'TenComplement')).toBe(true)
expect(seg.plan.some(p => p.rule === 'Cascade')).toBe(false)
})
it('ten-complement with cascade (199 + 1 = 200): has Cascade', () => {
const seq = generateUnifiedInstructionSequence(199, 200)
const seg = seq.segments.find(s => s.place === 0)!
expect(seg.plan.some(p => p.rule === 'Cascade')).toBe(true)
})
it('ranges: segment.termRange encloses all step.termPosition', () => {
const seq = generateUnifiedInstructionSequence(3478, 3500) // +22
const tensSeg = seq.segments.find(s => s.place === 1)!
expect(tensSeg).toBeDefined()
tensSeg.stepIndices.forEach(i => {
const r = seq.steps[i].termPosition
expect(r.startIndex >= tensSeg.termRange.startIndex).toBe(true)
expect(r.endIndex <= tensSeg.termRange.endIndex).toBe(true)
})
})
it('direct addition with multiple places maintains correct expressions', () => {
const seq = generateUnifiedInstructionSequence(0, 68) // +68
// Tens place: +6 should be "5 + 1" (direct)
const tensSeg = seq.segments.find(s => s.place === 1)
if (tensSeg) {
expect(tensSeg.expression.replace(/\s+/g, '')).toBe('50+10')
expect(tensSeg.plan.some(p => p.rule === 'Direct')).toBe(true)
}
// Ones place: +8 should be "5 + 3" (direct)
const onesSeg = seq.segments.find(s => s.place === 0)
if (onesSeg) {
expect(onesSeg.expression.replace(/\s+/g, '')).toBe('5+3')
expect(onesSeg.plan.some(p => p.rule === 'Direct')).toBe(true)
}
})
it('ensures no false FiveComplement classification for direct 6-9 adds', () => {
// Test all direct 6-9 additions to ensure they're never classified as FiveComplement
for (let digit = 6; digit <= 9; digit++) {
const seq = generateUnifiedInstructionSequence(0, digit)
const seg = seq.segments.find(s => s.place === 0)!
expect(seg.plan.some(p => p.rule === 'Direct')).toBe(true)
expect(seg.plan.some(p => p.rule === 'FiveComplement')).toBe(false)
expect(seg.expression).toMatch(/^5 \+ \d+$/)
}
})
it('validates complement expressions use parentheses', () => {
// Five complement
const fiveCompSeq = generateUnifiedInstructionSequence(2, 5)
const fiveCompSeg = fiveCompSeq.segments.find(s => s.place === 0)!
expect(fiveCompSeg.expression).toMatch(/^\(.+\)$/) // Should be wrapped in parentheses
// Ten complement
const tenCompSeq = generateUnifiedInstructionSequence(19, 20)
const tenCompSeg = tenCompSeq.segments.find(s => s.place === 0)!
expect(tenCompSeg.expression).toMatch(/^\(.+\)$/) // Should be wrapped in parentheses
})
})

View File

@@ -191,13 +191,18 @@ function decisionForTenComplement(a: number, d: number, nextIs9: boolean): Segme
}
function formatSegmentExpression(terms: string[]): string {
// single term -> "40"
if (terms.length === 1 && !terms[0].startsWith('-')) return terms[0]
if (terms.length === 0) return ''
// complement group -> "(pos - n1 - n2 - ...)"
const pos = terms[0]
const negs = terms.slice(1).map(t => t.replace(/^-/, ''))
return `(${pos} - ${negs.join(' - ')})`
const positives = terms.filter(t => !t.startsWith('-'))
const negatives = terms.filter(t => t.startsWith('-')).map(t => t.slice(1))
// All positive → join with pluses (no parentheses)
if (negatives.length === 0) {
return positives.join(' + ')
}
// Complement group → (pos - n1 - n2 - …)
return `(${positives[0]} - ${negatives.join(' - ')})`
}
function formatSegmentGoal(digit: number, placeValue: number): string {
@@ -260,34 +265,44 @@ function determineSegmentDecisions(
): SegmentDecision[] {
const sum = currentDigit + digit
if (steps.length === 1) {
// If there is exactly one step and it's positive, it's direct.
if (steps.length === 1 && !steps[0].operation.startsWith('-')) {
return [{
rule: 'Direct',
conditions: [`a+d=${currentDigit}+${digit}=${sum} ≤ 9`],
explanation: ['Fits in this place; add earth beads directly.']
explanation: ['Fits in this place; add beads directly.']
}]
}
const positives = steps.filter(s => !s.operation.startsWith('-')).map(s => parseInt(s.operation, 10))
const negatives = steps.filter(s => s.operation.startsWith('-')).map(s => Math.abs(parseInt(s.operation, 10)))
// No negatives → it's a direct (possibly 5+earth remainder) entry, not complement
if (negatives.length === 0) {
return [{
rule: 'Direct',
conditions: [`a+d=${currentDigit}+${digit}=${sum} ≤ 9`],
explanation: ['Heaven bead (5) plus lower beads: still direct addition.']
}]
}
// There are negatives → complement family
const hasFiveAdd = positives.some(v => Number.isInteger(v / 5) && isPowerOfTen(v / 5))
const tenAdd = positives.find(v => isPowerOfTenGE10(v))
const hasTenAdd = tenAdd !== undefined
if (hasFiveAdd && !hasTenAdd) {
return decisionForFiveComplement(currentDigit, digit)
}
if (hasTenAdd) {
const tenAddPlace = Math.round(Math.log10(tenAdd!))
// If the +10^k lands above the immediate next place, we must have rippled through 9s.
// Alternatively, multiple distinct higher places in negatives indicates cascade.
const negPlaces = new Set(negatives.map(v => Math.floor(Math.log10(v))))
const cascades = tenAddPlace > place + 1 || negPlaces.size >= 2
return decisionForTenComplement(currentDigit, digit, cascades)
}
if (hasFiveAdd) {
return decisionForFiveComplement(currentDigit, digit)
}
// Fallback (unlikely with current generators)
return [{
rule: 'Direct',
conditions: [`processing digit ${digit} at ${getPlaceName(place)}`],