feat: T4b/T4c wave 9 - phenology reports, autopilot A4-A10, honey ledger, Telegram delivery
- Phenology reports API with corrections and revokes; recompute per apiary via PhenologyRecomputeRequested, allowBackward for corrections; season changes are announced in the morning digest (changes since the previous digest, each once) - Season autopilot A4-A10 with per-apiary sync (Apiary before Colony locks) and WinterCheckRecorded - Honey batch ledger at runtime: initial amount, BOTTLING bookings, manual movements, one gram rounding, untracked bottlings never book - Telegram delivery: planning, Berlin delivery times incl. DST, quiet hours, permission re-check and stale skip at send time, rate limits, cleanup of old deliveries - ARCHITECTURE.md: assumptions 47-50 and updated sections 11.x Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
1971b2d9c4
commit
16cc557462
74 files changed
+9634
-340
No files matched your search
@@ -0,0 +1,49 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { join, relative, sep } from 'node:path';
|
||||
|
||||
// HoneyBatch.amount is the running rest of the movement book (spec 7.7): only
|
||||
// HoneyBatchLedgerService writes it. A second writer would break the
|
||||
// invariant amount = initialAmount + sum(quantity) / 1000.
|
||||
//
|
||||
// The regexes see an `amount` inside the call itself. A spread such as
|
||||
// `data: { ...dto }` is invisible to them; honey-batches.service.spec.ts
|
||||
// covers that case.
|
||||
const SRC = join(__dirname, '..');
|
||||
const LEDGER = 'src/honey-batches/honey-batch-ledger.service.ts';
|
||||
|
||||
function productionSources(dir: string): string[] {
|
||||
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
return productionSources(path);
|
||||
}
|
||||
return entry.name.endsWith('.ts') && !entry.name.endsWith('.spec.ts')
|
||||
? [path]
|
||||
: [];
|
||||
});
|
||||
}
|
||||
|
||||
function writersOf(...patterns: RegExp[]): string[] {
|
||||
return productionSources(SRC)
|
||||
.filter((file) => {
|
||||
const source = readFileSync(file, 'utf8');
|
||||
return patterns.some((pattern) => pattern.test(source));
|
||||
})
|
||||
.map((file) => `src/${relative(SRC, file).split(sep).join('/')}`)
|
||||
.sort();
|
||||
}
|
||||
|
||||
describe('honey batch amount writes', () => {
|
||||
it('honey batch amount is written only by the ledger', () =>
|
||||
expect(
|
||||
writersOf(
|
||||
/honeyBatch\.(update|updateMany|upsert)\([^)]*amount/s,
|
||||
/UPDATE "HoneyBatch"[^;]*"amount"/,
|
||||
),
|
||||
).toEqual([LEDGER]));
|
||||
|
||||
it('the guard sees a write: the ledger itself is found', () =>
|
||||
expect(writersOf(/honeyBatch\.update\(\{[^)]*data: \{ amount/s)).toContain(
|
||||
LEDGER,
|
||||
));
|
||||
});
|
||||
Reference in new issue
Block a user