ll-skills 1.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +90 -2
  2. package/README.md +134 -69
  3. package/agents/ll-executor.md +96 -0
  4. package/agents/ll-reviewer.md +76 -0
  5. package/agents/ll-scout.md +83 -0
  6. package/agents/ll-verifier.md +127 -0
  7. package/assets/preamble.md +70 -0
  8. package/assets/settings.suggested.json +19 -0
  9. package/bin/install.js +311 -54
  10. package/hooks/ll-precompact.js +49 -0
  11. package/hooks/ll-skills-check-update.js +1 -1
  12. package/hooks/ll-state.js +158 -0
  13. package/package.json +4 -2
  14. package/scripts/fixtures/empty/.gitkeep +0 -0
  15. package/scripts/fixtures/git-history.sh +49 -0
  16. package/scripts/fixtures/project/BACKLOG.md +11 -0
  17. package/scripts/fixtures/project/PLAN.md +66 -0
  18. package/scripts/fixtures/project/PROGRESS.md +57 -0
  19. package/scripts/fixtures/project/ROADMAP.md +28 -0
  20. package/scripts/fixtures/project/VERIFICATION.md +17 -0
  21. package/scripts/fixtures/project/decisions/DEC-0041-cents.md +16 -0
  22. package/scripts/fixtures/project/phases/07/PLAN.md +145 -0
  23. package/scripts/fixtures/project/src/a.ts +3 -0
  24. package/scripts/fixtures/project/src/pay.ts +5 -0
  25. package/scripts/fixtures/project/test/a.test.ts +5 -0
  26. package/scripts/ll-tools.js +664 -0
  27. package/scripts/smoke-test.sh +340 -0
  28. package/skills/ll-brainstorm/SKILL.md +180 -0
  29. package/skills/ll-brainstorm/references/decision-policy.md +118 -0
  30. package/skills/ll-brainstorm/references/techniques.md +75 -0
  31. package/skills/ll-close/SKILL.md +69 -0
  32. package/skills/ll-close/references/delivery.md +75 -0
  33. package/skills/ll-close/references/retrospective.md +48 -0
  34. package/skills/ll-decide/SKILL.md +135 -0
  35. package/skills/ll-decide/references/decision-policy.md +118 -0
  36. package/skills/ll-decide/references/decision-room.md +62 -0
  37. package/skills/ll-decide/references/disarm.md +79 -0
  38. package/skills/ll-decide/references/feedback-ingestion.md +90 -0
  39. package/skills/ll-decide/references/interview.md +102 -0
  40. package/skills/ll-decide/references/plan-skeleton.md +150 -0
  41. package/skills/ll-decide/references/premise-gate.md +83 -0
  42. package/skills/ll-decide/references/premortem.md +90 -0
  43. package/skills/ll-decide/references/review-spec.md +91 -0
  44. package/skills/ll-goal/SKILL.md +64 -0
  45. package/skills/ll-goal/references/goal-template.md +90 -0
  46. package/skills/ll-implement/SKILL.md +118 -0
  47. package/skills/ll-implement/references/briefs.md +123 -0
  48. package/skills/ll-implement/references/decision-policy.md +118 -0
  49. package/skills/ll-implement/references/phase-conversation.md +76 -0
  50. package/skills/ll-implement/references/phase-plan.md +111 -0
  51. package/skills/ll-oncall/SKILL.md +123 -0
  52. package/skills/ll-oncall/references/deploy-preflight.md +57 -0
  53. package/skills/ll-oncall/references/federation.md +110 -0
  54. package/skills/ll-oncall/references/watch-brief.md +39 -0
  55. package/skills/ll-refine/SKILL.md +116 -0
  56. package/skills/ll-refine/references/production-access.md +48 -0
  57. package/skills/ll-refine/references/visual-gate.md +100 -0
  58. package/skills/ll-research/SKILL.md +87 -0
  59. package/skills/ll-research/references/citation-check.md +38 -0
  60. package/skills/ll-research/references/front-brief.md +41 -0
  61. package/skills/ll-research/references/market-mode.md +83 -0
  62. package/skills/ll-resume/SKILL.md +75 -0
  63. package/skills/ll-update/SKILL.md +75 -0
  64. package/skills/ll-verify/SKILL.md +75 -0
  65. package/skills/ll-verify/references/verifier-briefs.md +116 -0
  66. package/agents/ll-implementador.md +0 -23
  67. package/skills/ll-atualizar/SKILL.md +0 -68
  68. package/skills/ll-decidir-antes/SKILL.md +0 -81
  69. package/skills/ll-decidir-antes/referencias/protocolo-entrevista.md +0 -112
  70. package/skills/ll-decidir-antes/referencias/template-spec.md +0 -238
  71. package/skills/ll-desarmar/SKILL.md +0 -254
  72. package/skills/ll-desarmar/referencias/execucao-adversarial.md +0 -217
  73. package/skills/ll-desarmar/referencias/humanos-e-substitutos.md +0 -116
  74. package/skills/ll-desarmar/referencias/placar-e-realimentacao.md +0 -140
  75. package/skills/ll-orquestrar/SKILL.md +0 -100
  76. package/skills/ll-pesquisar/SKILL.md +0 -159
  77. package/skills/ll-pesquisar/referencias/frente-de-pesquisa.md +0 -147
  78. package/skills/ll-pesquisar/referencias/sintese-e-fontes.md +0 -148
  79. package/skills/ll-pesquisar-mercado/SKILL.md +0 -112
  80. package/skills/ll-pesquisar-mercado/referencias/dossie.md +0 -375
  81. package/skills/ll-pesquisar-mercado/referencias/indice-e-fechamento.md +0 -122
  82. package/skills/ll-pesquisar-mercado/referencias/padroes-de-pesquisa.md +0 -149
  83. package/skills/ll-verificar-entrega/SKILL.md +0 -73
  84. package/skills/ll-verificar-entrega/referencias/briefs-auditoria.md +0 -291
  85. package/skills/ll-voltar-do-futuro/SKILL.md +0 -239
  86. package/skills/ll-voltar-do-futuro/referencias/anti-padroes-e-fundamentos.md +0 -201
  87. package/skills/ll-voltar-do-futuro/referencias/vetores-e-testes.md +0 -228
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // ll-state.js — ll-skills SessionStart hook. Injects the epilogue and the live
4
+ // state of the project into the new session. Reads stdin {cwd, source}.
5
+ // Never writes, never touches stderr, always exits 0.
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { execFileSync } = require('child_process');
10
+
11
+ const MAX_LINES = 60;
12
+ const J = path.join;
13
+
14
+ // <ll-shared:state>
15
+ // Copied verbatim into hooks/ll-state.js. Self-contained: only `fs` and `path`.
16
+ function readAnchoredBlock(file, name) {
17
+ let txt;
18
+ try { txt = fs.readFileSync(file, 'utf8'); } catch { return null; }
19
+ const all = txt.split('\n');
20
+ const open = new RegExp('^<!--\\s*' + name + '\\s*-->\\s*$');
21
+ const close = new RegExp('^<!--\\s*/' + name + '\\s*-->\\s*$');
22
+ let start = -1, end = -1;
23
+ for (let i = 0; i < all.length; i++) {
24
+ if (start < 0) { if (open.test(all[i])) start = i; } else if (close.test(all[i])) { end = i; break; }
25
+ }
26
+ if (start < 0 || end < 0) return null;
27
+ return { file, start, end, all, lines: all.slice(start + 1, end) };
28
+ }
29
+ function llSplitFlow(str) { // top-level commas only
30
+ const out = [];
31
+ let cur = '', depth = 0, q = null;
32
+ for (const ch of str) {
33
+ if (q) { cur += ch; if (ch === q) q = null; continue; }
34
+ if (ch === '"' || ch === "'") { q = ch; cur += ch; continue; }
35
+ if (ch === '[' || ch === '{') depth++;
36
+ else if (ch === ']' || ch === '}') depth--;
37
+ else if (ch === ',' && depth === 0) { out.push(cur); cur = ''; continue; }
38
+ cur += ch;
39
+ }
40
+ if (cur.trim() !== '') out.push(cur);
41
+ return out;
42
+ }
43
+ function llScalar(raw) {
44
+ const s = String(raw).trim();
45
+ if (/^\[[\s\S]*\]$/.test(s)) return llSplitFlow(s.slice(1, -1)).map(llScalar);
46
+ if (/^\{[\s\S]*\}$/.test(s)) {
47
+ const o = {};
48
+ for (const part of llSplitFlow(s.slice(1, -1))) {
49
+ const m = /^\s*([A-Za-z_][\w-]*)\s*:\s*([\s\S]*)$/.exec(part);
50
+ if (m) o[m[1]] = llScalar(m[2]);
51
+ }
52
+ return o;
53
+ }
54
+ if (/^(true|yes)$/i.test(s)) return true;
55
+ if (/^(false|no)$/i.test(s)) return false;
56
+ if (/^".*"$/.test(s) || /^'.*'$/.test(s)) return s.slice(1, -1);
57
+ return s;
58
+ }
59
+ function parseStateBlockLite(bl) {
60
+ const st = { phase: null, milestones: {}, order: [] };
61
+ let inM = false;
62
+ for (let i = 0; i < bl.length; i++) {
63
+ const line = bl[i].replace(/\s+$/, '');
64
+ if (!line.trim() || /^\s*#/.test(line)) continue;
65
+ let m = /^phase:\s*(.*)$/.exec(line);
66
+ if (m) { st.phase = String(llScalar(m[1])); inM = false; continue; }
67
+ if (/^milestones:\s*$/.test(line)) { inM = true; continue; }
68
+ m = /^(\s*)(M\d+|G-\d+):\s*\{(.*)\}\s*$/.exec(line);
69
+ if (m && inM) {
70
+ st.milestones[m[2]] = llScalar('{' + m[3] + '}');
71
+ st.order.push({ id: m[2], index: i, indent: m[1] });
72
+ } else if (/^\S/.test(line)) inM = false;
73
+ }
74
+ return st;
75
+ }
76
+ function boardFromState(st) {
77
+ const ids = Object.keys((st && st.milestones) || {});
78
+ const passed = ids.filter((i) => st.milestones[i].passes === true).length;
79
+ return { phase: st ? st.phase : null, passed, total: ids.length, list: ids,
80
+ text: 'phase ' + ((st && st.phase) || '?') + ' · milestones ' + passed + '/' + ids.length };
81
+ }
82
+ // </ll-shared:state>
83
+
84
+ function git(args, cwd) {
85
+ try {
86
+ return execFileSync('git', args, { cwd, timeout: 3000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
87
+ } catch { return ''; }
88
+ }
89
+ function nonEmpty(s) { return s.split('\n').filter((l) => l.trim() !== ''); }
90
+
91
+ function readStdin() {
92
+ try { return JSON.parse(fs.readFileSync(0, 'utf8')); } catch { return {}; }
93
+ }
94
+
95
+ // Last `## Epilogue` section of PROGRESS.md: {head: linesBefore, body: sectionLines}.
96
+ function splitEpilogue(all) {
97
+ let at = -1;
98
+ for (let i = 0; i < all.length; i++) if (/^## Epilogue/.test(all[i])) at = i;
99
+ if (at < 0) return { head: all, body: [] };
100
+ let end = all.length;
101
+ for (let i = at + 1; i < all.length; i++) if (/^## /.test(all[i])) { end = i; break; }
102
+ return { head: all.slice(0, at), body: nonEmpty(all.slice(at, end).join('\n')).slice(0, 25) };
103
+ }
104
+
105
+ function build(cwd, source) {
106
+ const progress = J(cwd, 'PROGRESS.md');
107
+ let raw = null;
108
+ try { raw = fs.readFileSync(progress, 'utf8'); } catch { raw = null; }
109
+ if (raw === null && !fs.existsSync(J(cwd, 'phases'))) return null;
110
+
111
+ const out = [];
112
+ const st = raw === null ? null : parseStateBlockLite((readAnchoredBlock(progress, 'll-state') || { lines: [] }).lines);
113
+ const board = boardFromState(st);
114
+ const phase = (st && st.phase) || 'NN';
115
+
116
+ if (source === 'compact') {
117
+ out.push('[post-compaction] re-read phases/' + phase + '/PLAN.md and the milestone board below before continuing; the files on disk are the truth, not the summary.');
118
+ }
119
+
120
+ const all = raw === null ? [] : raw.split('\n');
121
+ const { head, body } = splitEpilogue(all);
122
+ if (body.length) out.push('', ...body);
123
+ const recent = nonEmpty(head.join('\n')).slice(-15);
124
+ if (recent.length) out.push('', 'recent PROGRESS:', ...recent);
125
+
126
+ const status = nonEmpty(git(['status', '--short'], cwd));
127
+ if (status.length) {
128
+ out.push('', 'git status:', ...status.slice(0, 20));
129
+ if (status.length > 20) out.push(' (+' + (status.length - 20) + ' more)');
130
+ }
131
+ const wt = nonEmpty(git(['worktree', 'list'], cwd));
132
+ if (wt.length > 1) out.push('', 'worktrees:', ...wt);
133
+
134
+ let waiting = [];
135
+ try {
136
+ waiting = fs.readdirSync(J(cwd, 'decisions')).sort()
137
+ .filter((n) => /\.md$/.test(n) && /WAITING/.test(fs.readFileSync(J(cwd, 'decisions', n), 'utf8')))
138
+ .slice(0, 8);
139
+ } catch { waiting = []; }
140
+ if (waiting.length) out.push('', 'WAITING on you: ' + waiting.join(', '));
141
+
142
+ out.push('', board.text);
143
+ if (out.length <= MAX_LINES) return out;
144
+ return out.slice(0, MAX_LINES - 2).concat(['', board.text]);
145
+ }
146
+
147
+ function main() {
148
+ const input = readStdin();
149
+ const cwd = typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd();
150
+ const lines = build(cwd, input.source);
151
+ if (!lines || !lines.length) return;
152
+ process.stdout.write(JSON.stringify({
153
+ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: lines.join('\n').trim() },
154
+ }) + '\n');
155
+ }
156
+
157
+ try { main(); } catch { /* silence */ }
158
+ process.exit(0);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ll-skills",
3
- "version": "1.0.0",
4
- "description": "Pipeline de desenvolvimento orientado a evidência para Claude Code: pesquisa de mercado, premortem, POCs desarmadoras, spec por entrevista e verificação de entrega",
3
+ "version": "2.0.2",
4
+ "description": "Evidence-driven development pipeline for Claude Code: router preamble, 11 skills (brainstorm, research, decide, goal, implement, verify, close, resume, refine, oncall, update), 4 agents, 3 hooks and a state helper",
5
5
  "bin": {
6
6
  "ll-skills": "bin/install.js"
7
7
  },
@@ -10,6 +10,8 @@
10
10
  "skills",
11
11
  "agents",
12
12
  "hooks",
13
+ "scripts",
14
+ "assets",
13
15
  "CHANGELOG.md",
14
16
  "README.md"
15
17
  ],
File without changes
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # Builds a throwaway git repo from scripts/fixtures/project/ into <target-dir>.
3
+ # Five commits on five distinct days inside a nine-day span, in the order
4
+ # test(M1) -> feat(M1) -> feat(M10) -> feat(M2) -> test(M2)
5
+ # so that `tdd-gate M1` passes, `tdd-gate M2` fails and `phase-stats` reports 5 days with work.
6
+ # src/pay.ts is never touched after the first commit: VERIFICATION.md records its sha256.
7
+ set -euo pipefail
8
+
9
+ TARGET="${1:-}"
10
+ if [ -z "$TARGET" ]; then
11
+ echo "usage: git-history.sh <target-dir>" >&2
12
+ exit 2
13
+ fi
14
+
15
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ SRC="$HERE/project"
17
+
18
+ rm -rf "$TARGET"
19
+ mkdir -p "$TARGET"
20
+ cp -R "$SRC/." "$TARGET/"
21
+ cd "$TARGET"
22
+
23
+ G() { git -c user.name=fixture -c user.email=fixture@example.com -c commit.gpgsign=false "$@"; }
24
+
25
+ commit_on() { # commit_on <day-offset> <subject>
26
+ local day="$1" subject="$2" stamp
27
+ stamp="2026-09-0${day}T10:00:00+00:00"
28
+ GIT_AUTHOR_DATE="$stamp" GIT_COMMITTER_DATE="$stamp" G commit -q -m "$subject"
29
+ }
30
+
31
+ git init -q -b main .
32
+ G add -A
33
+ commit_on 1 "test(M1): red cases"
34
+
35
+ printf '\nexport const RECONCILE_VERSION = 2;\n' >> src/a.ts
36
+ G add src/a.ts
37
+ commit_on 3 "feat(M1): reconcile"
38
+
39
+ printf 'export const UNRELATED = true;\n' > src/unrelated.ts
40
+ G add src/unrelated.ts
41
+ commit_on 5 "feat(M10): unrelated"
42
+
43
+ printf 'export function ingest(lines: string[]): number {\n return lines.length;\n}\n' > src/ingest.ts
44
+ G add src/ingest.ts
45
+ commit_on 7 "feat(M2): thing"
46
+
47
+ printf "import { ingest } from '../src/ingest';\n\ntest('ingest counts lines', () => {\n expect(ingest(['a'])).toBe(1);\n});\n" > test/ingest.test.ts
48
+ G add test/ingest.test.ts
49
+ commit_on 9 "test(M2): late test"
@@ -0,0 +1,11 @@
1
+ # BACKLOG — fixture project
2
+
3
+ | id | born (phase / commit) | type | closing condition (executable) | state |
4
+ |---|---|---|---|---|
5
+ | B-014 | 07 / a1b2c3d | stub | `true` exit 0 | OPEN |
6
+ | B-015 | 07 / b2c3d4e | deviation | `false` exit 0 | OPEN |
7
+ | B-016 | 06 / c3d4e5f | debt | `true` exit 0 | CLOSED |
8
+
9
+ types: deviation | stub | test-not-run | debt | domain-question
10
+
11
+ Closed only by `ll-tools.js backlog-reconcile --run` (the condition passed), never by memory.
@@ -0,0 +1,66 @@
1
+ # PLAN — fixture project
2
+
3
+ ## §0 Precedence
4
+
5
+ This file is self-contained and the ONLY entry of the work. §2 > §3 > §4 > §5. Decisions in §3 are a
6
+ contract — do not re-litigate. Project CLAUDE.md > this file > phase plans > briefs.
7
+
8
+ ## §1 Objective and truths
9
+
10
+ One sentence of delivery: every provider event is reconciled exactly once and every mismatch is
11
+ visible to the operator.
12
+
13
+ - T1 Every provider event produces exactly one row in `billing_events`. — `npm test -- a.test.ts`
14
+ - T2 A replayed batch adds no rows. — `npm test -- replay.test.ts`
15
+ - T3 Cent-level mismatches are reported, never dropped. — `npm test -- pay.test.ts`
16
+
17
+ Requirements: REQ-k (phase 07), REQ-m (phase 08).
18
+
19
+ ## §2 Invariants
20
+
21
+ I-01 NEVER delete, disable or weaken a test or acceptance criterion. [owner, 2026-08-23]
22
+ I-02 Do not fill a gap with a plausible interpretation: report and ask. [owner, CLAUDE.md]
23
+
24
+ ## §3 Owner decisions
25
+
26
+ | id | question | decision | by | date | against recommendation? | reversible? |
27
+ |---|---|---|---|---|---|---|
28
+ | DEC-0041 | cents divergence | pending | — | — | — | 1 migration |
29
+
30
+ ## §4 Requirements from the premortem
31
+
32
+ R-01 A silent duplicate is worse than a rejected batch. (from F-01)
33
+
34
+ ## §5 Negative scope · freedoms · reserved
35
+
36
+ Out of scope: customer portal. Free: module names, migration layout. Owner only: production deploy.
37
+
38
+ ## §6 Global acceptance
39
+
40
+ CA-01 WHEN a batch is ingested twice THE SYSTEM SHALL keep one row per event · tolerance 0 ·
41
+ `npm test -- a.test.ts`
42
+
43
+ ## §7 Execution protocol
44
+
45
+ Session fable/high; scout sonnet/medium; contract executor opus/high; verifier opus/high.
46
+ Max 3 executors per wave. Push only with everything green.
47
+
48
+ ## §8 Phases
49
+
50
+ See ROADMAP.md. Current phase: 07.
51
+
52
+ ## §9 Environment
53
+
54
+ `BILLING_PROVIDER_KEY` (name only, never the value).
55
+
56
+ ## §10 What is left for this to survive without the owner
57
+
58
+ A provider sandbox key for the integration test.
59
+
60
+ ## §11 Sources
61
+
62
+ Owner conversation 2026-08-23; docs/research-billing/SUMMARY.md.
63
+
64
+ ## Errata
65
+
66
+ (append-only; empty)
@@ -0,0 +1,57 @@
1
+ # PROGRESS — fixture project
2
+
3
+ <!-- ll-state -->
4
+ phase: 07
5
+ milestones:
6
+ M1: { passes: true, commit: a1b2c3d, accepted_at: 2026-09-01T14:02:11Z }
7
+ M2: { passes: false, reason: "acceptance red: 2 tests fail" }
8
+ M3: { passes: false, state: ANSWERED_NO, reason: "spike ran; answer is no", propagates_to: [M5] }
9
+ <!-- /ll-state -->
10
+
11
+ ## Rules for all agents
12
+
13
+ - Only the main session writes this file. Executors return blocks; the session appends them.
14
+ - Never delete, disable or weaken a test or an acceptance criterion.
15
+ - Commit per milestone with `type(Mn): what`.
16
+
17
+ ## Phase 07
18
+
19
+ - [2026-09-01T13:40Z] phase 07 opened — billing reconciliation
20
+ - [2026-09-01T13:50Z] wave 1/3 — tracer + schema
21
+ - [2026-09-01T14:02Z] M1 acceptance green — npm test -- a.test.ts
22
+ - [2026-09-01T14:03Z] M1 marked passes true
23
+
24
+ ### M1 — 2026-09-01 14:02
25
+
26
+ built: reconcile() with idempotent key; 3 behavior cases
27
+ commits: test(M1): red cases · feat(M1): reconcile
28
+ commands: npm test -- a.test.ts → "Tests: 3 passed, 3 total"
29
+ deviations: none · questions: none · backlog: B-014 e2e cart not run (no fixture)
30
+ not_verified: backoff under real network latency
31
+
32
+ - [2026-09-03T09:12Z] wave 2/3 — reconciliation core
33
+ - [2026-09-03T09:40Z] spike for M3 started (cent-level mismatch)
34
+ - [2026-09-03T11:05Z] DEC-0041 opened — cents divergence, waiting on the owner
35
+ - [2026-09-05T08:30Z] M2 executor returned; acceptance red (2 tests fail)
36
+ - [2026-09-05T08:31Z] M2 marked passes false — reason recorded
37
+ - [2026-09-05T10:20Z] B-015 born — TODO markers left in src/billing
38
+ - [2026-09-05T16:44Z] spike for M3 answered: no
39
+ - [2026-09-07T09:00Z] M3 marked ANSWERED_NO — propagates to M5
40
+ - [2026-09-07T09:05Z] wave 3/3 held: M4, M5, M6 depend on the blocked branch
41
+ - [2026-09-07T12:18Z] plan-lint run on phases/07/PLAN.md — defects recorded
42
+ - [2026-09-07T15:10Z] file overlap noticed between M5 and M6 on src/pay.ts
43
+ - [compaction 2026-09-07T15:11Z · auto · HEAD b2c3d4e] re-read phases/07/PLAN.md and the milestone board before continuing.
44
+ - [2026-09-09T08:00Z] verification round for phase 07 started
45
+ - [2026-09-09T08:40Z] VERIFICATION.md written — one criterion stale
46
+ - [2026-09-09T09:15Z] backlog reviewed: B-014 and B-015 still open
47
+ - [2026-09-09T09:30Z] phase 07 stopped at the epilogue
48
+
49
+ ## Deferred verification
50
+
51
+ - SC-03 load under 10k rows · `ll-verify --criterion SC-03` · born phase 05
52
+
53
+ ## Epilogue — phase 07 — 2026-09-09
54
+
55
+ passed: M1 · left: M2 (acceptance red), M3 (BLOCKED: DEC-0041) · WAITING: DEC-0041 (cents rounding) ·
56
+ new backlog: B-014, B-015 · actions that need you: decide DEC-0041 before wave 3
57
+ ▶ Next — `/clear`, then `ll-implement 8`
@@ -0,0 +1,28 @@
1
+ # ROADMAP — fixture project
2
+
3
+ | phase | name | depends_on | requirements | state |
4
+ |---|---|---|---|---|
5
+ | 05 | payment intake | — | REQ-a | DONE (docs/history/v1.0) |
6
+ | 06 | provider adapters | 05 | REQ-b | DONE (docs/history/v1.0) |
7
+ | 07 | billing reconciliation | 05, 06 | REQ-k | ACTIVE |
8
+ | 08 | reconciliation reporting | 07 | REQ-m | PLANNED |
9
+
10
+ ## Phase 07 — billing reconciliation
11
+
12
+ Objective: every provider event lands in `billing_events` exactly once and mismatches are visible.
13
+
14
+ Success criteria:
15
+ - SC-01 The reconciliation job ingests a provider batch and writes one row per event.
16
+ - SC-02 A replayed batch produces no duplicate rows.
17
+ - SC-03 Cent-level mismatches are reported instead of silently dropped.
18
+ - SC-04 The operator can list tolerated rows for a day.
19
+
20
+ Deferred ideas: customer portal; accounting export.
21
+
22
+ ## Phase 08 — reconciliation reporting
23
+
24
+ Objective: the operator reads yesterday's reconciliation without opening the database.
25
+
26
+ Success criteria:
27
+ - SC-01 A daily report lists ingested, tolerated and rejected rows.
28
+ - SC-02 The report is reachable from the operator console.
@@ -0,0 +1,17 @@
1
+ # VERIFICATION — phase 07 — 2026-09-09
2
+
3
+ verdict: APPROVED_WITH_RESERVATIONS · product: OK · process: FAIL (I-01 weakened without a DEC)
4
+
5
+ | C | criterion | command | exit | file:line | sha256 | freshness | state |
6
+ |---|---|---|---|---|---|---|---|
7
+ | SC-01 | one row per provider event | npm test -- a.test.ts | 0 | src/pay.ts:3 | sha256:946c8f6b2d9a | FRESH | VERIFIED |
8
+ | SC-02 | a replayed batch adds no rows | npm test -- ingest.test.ts | 0 | src/a.ts:1 | sha256:0000dead1234 | FRESH | VERIFIED |
9
+ | SC-03 | cent mismatches are reported | — | — | test/a.test.ts:3 | — | — | PRESENT_NO_BEHAVIOR |
10
+
11
+ BLOCKS: process — the tolerance assertion in `test/a.test.ts` was loosened with no DEC.
12
+
13
+ Disconfirmation: 1 partial requirement (SC-02 happy path only); 1 uncovered error path (provider timeout).
14
+
15
+ What this verification does NOT prove: behavior under real provider latency.
16
+
17
+ Deferred: SC-03 until the fixture exists (BACKLOG B-014).
@@ -0,0 +1,16 @@
1
+ # DEC-0041 — cents divergence: reject the line or accept and flag?
2
+
3
+ - class: QUESTION · layer: product · impact: HIGH · revert: 1 migration
4
+ - grounding: internal (src/pay.ts:1; 3 cases in test/) | external (research-billing/F02, 2026-09-05)
5
+ - options:
6
+
7
+ | option | what starts to hold | cost | what is lost | revert |
8
+ |---|---|---|---|---|
9
+ | A accept + flag (recommended) | the KPI the owner reads stays whole | 1 day | strict totals | 1 commit |
10
+ | B reject the line | totals are exact | 2 days | manual queue grows | 1 migration |
11
+
12
+ - depends_on: — · conditions: M3, M5
13
+ - status: WAITING — asked 2026-09-03T11:05Z, no answer yet
14
+ - owner's words (verbatim): —
15
+ - consequences by rule: SC-03 in ROADMAP measures "tolerated rows" only under option A.
16
+ - superseded_by: —
@@ -0,0 +1,145 @@
1
+ # Phase 07 — billing reconciliation
2
+
3
+ Objective (from ROADMAP): every provider event lands in `billing_events` exactly once and mismatches
4
+ are visible. Success criteria: SC-01..SC-04 (pasted from ROADMAP.md).
5
+ Decisions: phases/07/DECISIONS.md · Context: phases/07/CODE-CONTEXT.md
6
+
7
+ ## Milestones
8
+
9
+ <!-- ll-milestones -->
10
+ milestones:
11
+ - id: M1
12
+ name: end-to-end tracer
13
+ files: [src/a.ts, test/a.test.ts]
14
+ depends_on: []
15
+ tdd: yes
16
+ acceptance: "npm test -- a.test.ts"
17
+ stop: none
18
+ truths: [T1]
19
+ exclusive: []
20
+ model: opus/high
21
+ verification: internal
22
+ - id: M2
23
+ name: provider batch ingest
24
+ files: [src/ingest.ts, test/ingest.test.ts]
25
+ depends_on: [M1]
26
+ tdd: yes
27
+ acceptance: "npm test -- ingest.test.ts"
28
+ stop: none
29
+ truths: [T2]
30
+ exclusive: []
31
+ model: opus/high
32
+ verification: internal
33
+ - id: M3
34
+ name: cent mismatch spike
35
+ files: [src/spike.ts]
36
+ depends_on: [M1]
37
+ tdd: no
38
+ acceptance: "npm run spike -- cents"
39
+ stop: none
40
+ truths: [T3]
41
+ exclusive: []
42
+ model: sonnet/medium
43
+ verification: internal
44
+ - id: M4
45
+ name: reconcile join
46
+ files: [src/join.ts, test/join.test.ts]
47
+ depends_on: [M2, M3]
48
+ tdd: yes
49
+ acceptance: "npm test -- join.test.ts"
50
+ stop: none
51
+ truths: [T1, T2]
52
+ exclusive: [test:db]
53
+ model: opus/high
54
+ verification: internal
55
+ - id: M5
56
+ name: tolerated rows report
57
+ files: [src/pay.ts, test/pay.test.ts]
58
+ depends_on: [M3]
59
+ tdd: yes
60
+ acceptance: "npm test -- pay.test.ts"
61
+ stop: owner
62
+ truths: [T3]
63
+ exclusive: []
64
+ model: opus/high
65
+ verification: external
66
+ - id: M6
67
+ name: mismatch flag column
68
+ files: [src/pay.ts, src/schema.ts]
69
+ depends_on: [M3]
70
+ tdd: no
71
+ stop: none
72
+ truths: [T3]
73
+ exclusive: [test:db]
74
+ model: sonnet/medium
75
+ verification: internal
76
+ - id: M7
77
+ name: operator listing
78
+ files: [src/list.ts, test/list.test.ts]
79
+ depends_on: [M9, M4]
80
+ tdd: yes
81
+ acceptance: "npm test -- list.test.ts"
82
+ stop: none
83
+ truths: [T1]
84
+ exclusive: []
85
+ model: sonnet/medium
86
+ verification: internal
87
+ <!-- /ll-milestones -->
88
+
89
+ ### M1 — end-to-end tracer
90
+
91
+ read_first:
92
+ - src/a.ts:1-3 — the shape to copy for the batch signature
93
+ - test/a.test.ts:1-5 — test layout
94
+ action: export `reconcile(batch)` keyed by `(provider, event_id)`.
95
+ behavior (tdd: yes): duplicate event, new event, empty batch.
96
+ acceptance: `npm test -- a.test.ts` exit 0; last line pasted.
97
+
98
+ ### M2 — provider batch ingest
99
+
100
+ read_first:
101
+ - src/a.ts:1-3 — the reconcile entry point
102
+ action: read a provider batch file and hand each event to `reconcile`.
103
+ behavior (tdd: yes): malformed line, empty file, 3-event file.
104
+ acceptance: `npm test -- ingest.test.ts` exit 0.
105
+
106
+ ### M3 — cent mismatch spike
107
+
108
+ read_first:
109
+ - src/pay.ts:1-5 — the current tolerance rule
110
+ action: measure how many rows of the September batch differ by cents only.
111
+ acceptance: `npm run spike -- cents` exit 0 with the count on the last line.
112
+
113
+ ### M4 — reconcile join
114
+
115
+ read_first:
116
+ - src/a.ts:1-3 — reconcile signature
117
+ action: join ingested events with the ledger rows; keep the idempotency key.
118
+ behavior (tdd: yes): matched, unmatched, duplicate.
119
+ acceptance: `npm test -- join.test.ts` exit 0.
120
+
121
+ ### M5 — tolerated rows report
122
+
123
+ read_first:
124
+ - src/pay.ts:1-5 — `tolerated()` is the rule to reuse
125
+ action: list rows tolerated for a day; the owner unblocks the format.
126
+ behavior (tdd: yes): under tolerance, over tolerance, exactly at tolerance.
127
+ acceptance: `npm test -- pay.test.ts` exit 0.
128
+
129
+ ### M6 — mismatch flag column
130
+
131
+ read_first:
132
+ - src/pay.ts:1-5 — where the flag is computed
133
+ action: add the `mismatch_flag` column and write it from `tolerated()`.
134
+
135
+ ### M7 — operator listing
136
+
137
+ read_first:
138
+ - src/pay.ts:1-5 — the tolerated rule the listing filters on
139
+ action: expose the tolerated rows through the operator console.
140
+ behavior (tdd: yes): empty day, one row, paging.
141
+ acceptance: `npm test -- list.test.ts` exit 0.
142
+
143
+ ## Waves (printed by `ll-tools.js waves`)
144
+
145
+ | wave | milestones | builds |
@@ -0,0 +1,3 @@
1
+ export function reconcile(batch: string[]): string[] {
2
+ return Array.from(new Set(batch));
3
+ }
@@ -0,0 +1,5 @@
1
+ export type Mismatch = { eventId: string; cents: number };
2
+
3
+ export function tolerated(m: Mismatch): boolean {
4
+ return Math.abs(m.cents) <= 5;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { reconcile } from '../src/a';
2
+
3
+ test('reconcile dedupes a replayed batch', () => {
4
+ expect(reconcile(['e1', 'e1', 'e2'])).toEqual(['e1', 'e2']);
5
+ });