feat(vida): página "Dia a dia" com as decisões e o porquê

Cada decisão tomada com base no que ele aprendeu passa a ser guardada
com o motivo (aprendizado.decidir, até 800, persistidas):
- acordou do cochilo antes da hora: "às 18h de quarta costuma ter
  gente, pelo plano que você contou";
- cochilo com a casa vazia, com a duração e o porquê;
- sensor segurado numa hora em que acordou à toa (no máximo 1 nota a
  cada 10 min);
- gesto escolhido porque costuma agradar (chance ≥ ×1,3);
- fechamento do dia: horas com gente, carinhos e quanto o dia conferiu
  com o plano;
- análise da IA (adotada ou sugestão, com a porcentagem) e rotina
  contada por você.

esperado_e_fonte diz de onde veio a expectativa (plano seu, plano da
IA, o que viu naquele dia da semana ou média). GET /aprendizado/dia
devolve um dia: medido × plano por hora, conferência do dia, resumo e
decisões.

Painel: tela "Dia a dia" (#/dia-a-dia) com navegação entre dias, os
números do dia, as faixas "Viu" × "Esperava" e a linha do tempo das
decisões; atalho no cartão "O que ele aprendeu", no Ctrl+K e no guia.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-09 17:34:30 -03:00
co-authored by Claude Opus 5.5
parent 935bf8d586
commit b4cf13df11
10 changed files with 553 additions and 13 deletions
+58
View File
@@ -336,6 +336,64 @@ class SemanaTest(unittest.TestCase):
v.visto_em = em(14, 9)
atos = [v.agir(em(14, 10) + i * 120) for i in range(10)]
self.assertIn('atividade dormir', atos)
cochilo = next(d for d in self.a.decisoes if d['tipo'] == 'cochilo')
self.assertEqual(cochilo['motivo'], 'às 10h de quarta a casa costuma ficar vazia, pelo plano que você contou')
class DiaADiaTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.a = Aprendizado(Path(self.tmp.name) / 'ap.json')
self.a.ia, self.a.pode_ia = (lambda provedor, prompt, schema: plano_certo()), (lambda provedor: True)
self.a.config['analise_ia'] = False
self.v = Vida(Path(self.tmp.name) / 'v.json', iniciar=False, rng=random.Random(2))
self.v.aprendizado = self.a
self.v.enfileirar = lambda ordem: None
def tearDown(self):
self.tmp.cleanup()
def test_cada_decisao_guarda_o_porque(self):
semana_tipica(self.a)
self.a.analisar(em(19, 1))
self.assertEqual(self.a.decisoes[0]['tipo'], 'ia')
self.assertIn('plano adotado: confere', self.a.decisoes[0]['motivo'])
# acordar antes da chegada (quarta, 17h50)
self.v.dormindo_proprio = True
self.v.necessidades['energia'] = 70
self.assertEqual(self.v.agir(em(14, 17, 50)), 'acordar')
self.assertEqual(self.a.decisoes[0]['motivo'], 'às 18h de quarta costuma ter gente, pelo plano da IA')
# sensor segurado numa hora em que acordou à toa
self.a.falsos[22] = 1.0
self.v.pir_acorda(em(14, 22))
self.v.pir_acorda(em(14, 22) + 30) # a 2ª nota em menos de 10 min não repete
notas = [d for d in self.a.decisoes if d['tipo'] == 'sono']
self.assertEqual(len(notas), 1)
self.assertIn('pede 1 movimento a mais', notas[0]['motivo'])
def test_fechar_o_dia_resume_e_confere_com_o_plano(self):
self.a.informar('Fico fora de terça a quinta, das 8h às 18h')
semana_tipica(self.a, semanas=1)
fechou = [d for d in self.a.decisoes if d['tipo'] == 'dia']
self.assertEqual(len(fechou), 7)
quarta = next(d for d in fechou if 'quarta' in d['texto'])
self.assertIn('conferiu 100% com o plano', quarta['motivo'])
def test_dia_a_dia_de_um_dia_passado_e_de_hoje(self):
semana_tipica(self.a)
self.a.analisar(em(19, 1))
d = self.a.dia_a_dia('2026-10-14')
self.assertEqual(d['nome_dia'], 'quarta')
self.assertFalse(d['hoje'])
self.assertLess(d['medido'][10], 0.3)
self.assertEqual(d['plano'][2], dict(ini=8, fim=18, tipo='fora'))
self.assertEqual(d['confere'], 1.0)
self.assertTrue(any(x['tipo'] == 'dia' for x in d['decisoes']))
self.assertIn('2026-10-05', d['dias'])
hoje = self.a.dia_a_dia()
self.assertTrue(hoje['hoje'])
with self.assertRaises(ValueError):
self.a.dia_a_dia('ontem')
class RotasTest(unittest.TestCase):