crewly 1.20.301 → 1.20.303
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.
- package/config/roles/developer/prompt.md +7 -0
- package/config/roles/fullstack-dev/prompt.md +7 -0
- package/config/roles/product-manager/prompt.md +7 -0
- package/config/skills/agent/core/receive-file/SKILL.md +4 -0
- package/config/skills/agent/core/request-file/SKILL.md +74 -0
- package/config/skills/agent/core/request-file/execute.sh +89 -0
- package/config/skills/agent/core/request-file/execute.test.sh +77 -0
- package/config/skills/agent/core/slack-file/SKILL.md +1 -1
- package/config/skills/orchestrator/usage-categories/SKILL.md +59 -0
- package/config/skills/orchestrator/usage-categories/execute.sh +81 -0
- package/config/skills/orchestrator/usage-categories/execute.test.sh +45 -0
- package/config/skills/orchestrator/usage-report/SKILL.md +71 -0
- package/config/skills/orchestrator/usage-report/execute.sh +84 -0
- package/config/skills/orchestrator/usage-report/execute.test.sh +39 -0
- package/config/skills/team-leader/usage-report/SKILL.md +71 -0
- package/config/skills/team-leader/usage-report/execute.sh +84 -0
- package/config/skills/team-leader/usage-report/execute.test.sh +39 -0
- package/dist/backend/backend/src/constants.d.ts +24 -4
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +29 -6
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts +9 -0
- package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.controller.js +21 -0
- package/dist/backend/backend/src/controllers/apps/apps.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts +2 -0
- package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.routes.js +7 -1
- package/dist/backend/backend/src/controllers/apps/apps.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.d.ts +36 -0
- package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.js +158 -0
- package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.d.ts +15 -0
- package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.js +26 -0
- package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.js.map +1 -0
- package/dist/backend/backend/src/index.d.ts +10 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +75 -17
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +3 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/agent/follow-through.wiring.d.ts +32 -0
- package/dist/backend/backend/src/services/agent/follow-through.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/follow-through.wiring.js +42 -1
- package/dist/backend/backend/src/services/agent/follow-through.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts +26 -6
- package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/resource-mode.service.js +59 -5
- package/dist/backend/backend/src/services/agent/resource-mode.service.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/backend/backend/src/services/apps/apps.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/apps/apps.wiring.js +14 -0
- package/dist/backend/backend/src/services/apps/apps.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/apps/file-request-watch.service.d.ts +88 -0
- package/dist/backend/backend/src/services/apps/file-request-watch.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/apps/file-request-watch.service.js +170 -0
- package/dist/backend/backend/src/services/apps/file-request-watch.service.js.map +1 -0
- package/dist/backend/backend/src/services/monitoring/agent-turn-state.d.ts +35 -0
- package/dist/backend/backend/src/services/monitoring/agent-turn-state.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/agent-turn-state.js +101 -0
- package/dist/backend/backend/src/services/monitoring/agent-turn-state.js.map +1 -1
- package/dist/backend/backend/src/services/monitoring/background-work-watchdog.d.ts +58 -0
- package/dist/backend/backend/src/services/monitoring/background-work-watchdog.d.ts.map +1 -0
- package/dist/backend/backend/src/services/monitoring/background-work-watchdog.js +100 -0
- package/dist/backend/backend/src/services/monitoring/background-work-watchdog.js.map +1 -0
- package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/token-usage.service.js +3 -0
- package/dist/backend/backend/src/services/monitoring/token-usage.service.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts +7 -0
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js +15 -0
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +51 -33
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts +24 -0
- package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js +63 -1
- package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
- package/dist/backend/backend/src/services/trace/trace-context.service.d.ts +42 -2
- package/dist/backend/backend/src/services/trace/trace-context.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/trace/trace-context.service.js +66 -5
- package/dist/backend/backend/src/services/trace/trace-context.service.js.map +1 -1
- package/dist/backend/backend/src/services/trace/trace-recorder.d.ts.map +1 -1
- package/dist/backend/backend/src/services/trace/trace-recorder.js +11 -0
- package/dist/backend/backend/src/services/trace/trace-recorder.js.map +1 -1
- package/dist/backend/backend/src/services/usage-ledger/categories-param.d.ts +28 -0
- package/dist/backend/backend/src/services/usage-ledger/categories-param.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/categories-param.js +67 -0
- package/dist/backend/backend/src/services/usage-ledger/categories-param.js.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/excerpt.d.ts +37 -0
- package/dist/backend/backend/src/services/usage-ledger/excerpt.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/excerpt.js +49 -0
- package/dist/backend/backend/src/services/usage-ledger/excerpt.js.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/index.d.ts +14 -0
- package/dist/backend/backend/src/services/usage-ledger/index.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/index.js +14 -0
- package/dist/backend/backend/src/services/usage-ledger/index.js.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.d.ts +44 -0
- package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.js +52 -0
- package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.js.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.d.ts +100 -0
- package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.js +189 -0
- package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.js.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/owner-spans.d.ts +41 -0
- package/dist/backend/backend/src/services/usage-ledger/owner-spans.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/owner-spans.js +71 -0
- package/dist/backend/backend/src/services/usage-ledger/owner-spans.js.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/team-directory.d.ts +47 -0
- package/dist/backend/backend/src/services/usage-ledger/team-directory.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/team-directory.js +106 -0
- package/dist/backend/backend/src/services/usage-ledger/team-directory.js.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/unit-classifier.d.ts +101 -0
- package/dist/backend/backend/src/services/usage-ledger/unit-classifier.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/unit-classifier.js +263 -0
- package/dist/backend/backend/src/services/usage-ledger/unit-classifier.js.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.d.ts +96 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.js +166 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.js.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.d.ts +90 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.js +294 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.js.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.d.ts +102 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.js +314 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.js.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.d.ts +62 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.js +106 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.js.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.d.ts +52 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.js +95 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.js.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.d.ts +127 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.js +19 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.js.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-report.service.d.ts +124 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-report.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-report.service.js +372 -0
- package/dist/backend/backend/src/services/usage-ledger/usage-report.service.js.map +1 -0
- package/dist/backend/backend/src/types/slack.types.d.ts +8 -0
- package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/slack.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +24 -4
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +29 -6
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts +26 -6
- package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/resource-mode.service.js +59 -5
- package/dist/cli/backend/src/services/agent/resource-mode.service.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/cli/backend/src/services/monitoring/agent-turn-state.d.ts +35 -0
- package/dist/cli/backend/src/services/monitoring/agent-turn-state.d.ts.map +1 -1
- package/dist/cli/backend/src/services/monitoring/agent-turn-state.js +101 -0
- package/dist/cli/backend/src/services/monitoring/agent-turn-state.js.map +1 -1
- package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/monitoring/token-usage.service.js +3 -0
- package/dist/cli/backend/src/services/monitoring/token-usage.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +51 -33
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts +24 -0
- package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js +63 -1
- package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
- package/dist/cli/backend/src/services/trace/trace-context.service.d.ts +42 -2
- package/dist/cli/backend/src/services/trace/trace-context.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/trace/trace-context.service.js +66 -5
- package/dist/cli/backend/src/services/trace/trace-context.service.js.map +1 -1
- package/dist/cli/backend/src/services/trace/trace-recorder.d.ts.map +1 -1
- package/dist/cli/backend/src/services/trace/trace-recorder.js +11 -0
- package/dist/cli/backend/src/services/trace/trace-recorder.js.map +1 -1
- package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.d.ts +44 -0
- package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.d.ts.map +1 -0
- package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.js +52 -0
- package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.js.map +1 -0
- package/dist/cli/backend/src/services/worktree/low-disk-guard.d.ts +109 -0
- package/dist/cli/backend/src/services/worktree/low-disk-guard.d.ts.map +1 -0
- package/dist/cli/backend/src/services/worktree/low-disk-guard.js +148 -0
- package/dist/cli/backend/src/services/worktree/low-disk-guard.js.map +1 -0
- package/dist/cli/backend/src/services/worktree/worktree-janitor.git.d.ts +224 -0
- package/dist/cli/backend/src/services/worktree/worktree-janitor.git.d.ts.map +1 -0
- package/dist/cli/backend/src/services/worktree/worktree-janitor.git.js +449 -0
- package/dist/cli/backend/src/services/worktree/worktree-janitor.git.js.map +1 -0
- package/dist/cli/backend/src/types/slack.types.d.ts +8 -0
- package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/slack.types.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Puts tickets into categories at report time.
|
|
3
|
+
*
|
|
4
|
+
* Order of attempts, cheapest first:
|
|
5
|
+
* 1. Deterministic: keywords (or the comma-separated phrases of the
|
|
6
|
+
* description), team and project hints.
|
|
7
|
+
* 2. Only for tickets nothing matched, and only when a model is available: one
|
|
8
|
+
* small batched question to the default cheap model.
|
|
9
|
+
* 3. Otherwise `other`.
|
|
10
|
+
*
|
|
11
|
+
* With no definitions the category is the ticket's team (else its project).
|
|
12
|
+
*
|
|
13
|
+
* Results are cached per (category-set id, ticket); a fallback `other` that
|
|
14
|
+
* came from having no model is not cached, so it is retried later. The cache
|
|
15
|
+
* lives beside the ledger and is never part of the ticket facts.
|
|
16
|
+
*
|
|
17
|
+
* @module services/usage-ledger/unit-classifier
|
|
18
|
+
*/
|
|
19
|
+
import { promises as fs } from 'fs';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { atomicWriteJson } from '../../utils/file-io.utils.js';
|
|
22
|
+
import { categorySetId } from './usage-categories.store.js';
|
|
23
|
+
/** Category of tickets that match nothing. */
|
|
24
|
+
export const OTHER_CATEGORY = 'other';
|
|
25
|
+
/** Category of usage that belongs to no ticket. */
|
|
26
|
+
export const NO_TICKET_CATEGORY = '(no ticket)';
|
|
27
|
+
/** File with the classification cache. */
|
|
28
|
+
export const CLASSIFICATION_CACHE_FILE = 'classification-cache.json';
|
|
29
|
+
/** Sets kept in the cache (oldest dropped). */
|
|
30
|
+
const MAX_CACHED_SETS = 6;
|
|
31
|
+
/** Tickets sent to the model in one call, and calls per report. */
|
|
32
|
+
const LLM_BATCH = 25;
|
|
33
|
+
const LLM_MAX_CALLS = 2;
|
|
34
|
+
/** Default classification (no user categories): the ticket's team, else its project. */
|
|
35
|
+
export function defaultCategory(unit) {
|
|
36
|
+
return unit.team?.trim() || unit.project?.trim() || OTHER_CATEGORY;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Keywords of a definition: its explicit keywords, else the comma / semicolon
|
|
40
|
+
* separated phrases of its description (so "Xiaohongshu, Substack, LinkedIn"
|
|
41
|
+
* works as typed).
|
|
42
|
+
*
|
|
43
|
+
* @param def - Category definition
|
|
44
|
+
* @returns Lower-case keywords
|
|
45
|
+
*/
|
|
46
|
+
export function keywordsOf(def) {
|
|
47
|
+
const explicit = def.keywords ?? [];
|
|
48
|
+
const phrases = explicit.length > 0
|
|
49
|
+
? []
|
|
50
|
+
: (def.description ?? '')
|
|
51
|
+
.split(/[,;,;、\n]+/)
|
|
52
|
+
.map((p) => p.trim())
|
|
53
|
+
.filter((p) => p.length >= 2 && p.length <= 40);
|
|
54
|
+
return [...new Set([...explicit, ...phrases].map((k) => k.toLowerCase()))];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether a lower-case text contains a keyword. Short ASCII keywords must
|
|
58
|
+
* match whole words ("ai" must not match "email").
|
|
59
|
+
*/
|
|
60
|
+
function hasKeyword(text, keyword) {
|
|
61
|
+
if (/^[a-z0-9 ]+$/.test(keyword) && keyword.length <= 3) {
|
|
62
|
+
return new RegExp(`(^|[^a-z0-9])${keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^a-z0-9]|$)`).test(text);
|
|
63
|
+
}
|
|
64
|
+
return text.includes(keyword);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Best category by keyword / team / project hints.
|
|
68
|
+
*
|
|
69
|
+
* @param unit - The ticket
|
|
70
|
+
* @param defs - Definitions
|
|
71
|
+
* @returns Category name, or null when nothing matched
|
|
72
|
+
*/
|
|
73
|
+
export function matchCategory(unit, defs) {
|
|
74
|
+
const text = [unit.title, unit.excerpt, unit.ticketId, unit.team, unit.project].filter(Boolean).join(' \n ').toLowerCase();
|
|
75
|
+
const team = (unit.team ?? '').toLowerCase();
|
|
76
|
+
const project = (unit.project ?? '').toLowerCase();
|
|
77
|
+
let best = null;
|
|
78
|
+
for (const def of defs) {
|
|
79
|
+
let score = 0;
|
|
80
|
+
for (const k of keywordsOf(def))
|
|
81
|
+
if (hasKeyword(text, k))
|
|
82
|
+
score += 1;
|
|
83
|
+
if (team && def.teams?.some((t) => t.toLowerCase() === team))
|
|
84
|
+
score += 2;
|
|
85
|
+
if (project && def.projects?.some((p) => p.toLowerCase() === project))
|
|
86
|
+
score += 2;
|
|
87
|
+
if (score > 0 && (!best || score > best.score))
|
|
88
|
+
best = { name: def.name, score };
|
|
89
|
+
}
|
|
90
|
+
return best?.name ?? null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Classifies tickets for reports.
|
|
94
|
+
*/
|
|
95
|
+
export class UnitClassifier {
|
|
96
|
+
llm;
|
|
97
|
+
cacheFile;
|
|
98
|
+
cache = null;
|
|
99
|
+
/**
|
|
100
|
+
* @param dir - Ledger directory
|
|
101
|
+
* @param llm - Optional model (null: deterministic only)
|
|
102
|
+
*/
|
|
103
|
+
constructor(dir, llm = null) {
|
|
104
|
+
this.llm = llm;
|
|
105
|
+
this.cacheFile = path.join(dir, CLASSIFICATION_CACHE_FILE);
|
|
106
|
+
}
|
|
107
|
+
async loadCache() {
|
|
108
|
+
if (this.cache)
|
|
109
|
+
return this.cache;
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(await fs.readFile(this.cacheFile, 'utf-8'));
|
|
112
|
+
this.cache = parsed && typeof parsed.sets === 'object' && parsed.sets ? parsed : { sets: {} };
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
this.cache = { sets: {} };
|
|
116
|
+
}
|
|
117
|
+
return this.cache;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Classify tickets.
|
|
121
|
+
*
|
|
122
|
+
* @param units - Tickets (never the "(no ticket)" pseudo-units)
|
|
123
|
+
* @param defs - The user's categories; empty means the default (team / project)
|
|
124
|
+
* @returns Category per ticket
|
|
125
|
+
*/
|
|
126
|
+
async classify(units, defs) {
|
|
127
|
+
const categories = new Map();
|
|
128
|
+
if (defs.length === 0) {
|
|
129
|
+
for (const u of units)
|
|
130
|
+
categories.set(u.unitId, defaultCategory(u));
|
|
131
|
+
return { categories, llmClassified: 0, unclassified: 0 };
|
|
132
|
+
}
|
|
133
|
+
const setId = categorySetId(defs);
|
|
134
|
+
const cache = await this.loadCache();
|
|
135
|
+
const cached = cache.sets[setId] ?? {};
|
|
136
|
+
const fresh = {};
|
|
137
|
+
const unmatched = [];
|
|
138
|
+
for (const u of units) {
|
|
139
|
+
const known = cached[u.unitId];
|
|
140
|
+
if (known) {
|
|
141
|
+
categories.set(u.unitId, known);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const hit = matchCategory(u, defs);
|
|
145
|
+
if (hit) {
|
|
146
|
+
categories.set(u.unitId, hit);
|
|
147
|
+
fresh[u.unitId] = hit;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
unmatched.push(u);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
let llmClassified = 0;
|
|
154
|
+
let unclassified = 0;
|
|
155
|
+
const answers = this.llm ? await this.askModel(unmatched, defs) : new Map();
|
|
156
|
+
for (const u of unmatched) {
|
|
157
|
+
const answer = answers.get(u.unitId);
|
|
158
|
+
if (answer) {
|
|
159
|
+
categories.set(u.unitId, answer);
|
|
160
|
+
fresh[u.unitId] = answer;
|
|
161
|
+
if (answer !== OTHER_CATEGORY)
|
|
162
|
+
llmClassified += 1;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
categories.set(u.unitId, OTHER_CATEGORY);
|
|
166
|
+
unclassified += 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (Object.keys(fresh).length > 0)
|
|
170
|
+
await this.saveCache(setId, fresh);
|
|
171
|
+
return { categories, llmClassified, unclassified };
|
|
172
|
+
}
|
|
173
|
+
async saveCache(setId, fresh) {
|
|
174
|
+
const cache = await this.loadCache();
|
|
175
|
+
const merged = { ...(cache.sets[setId] ?? {}), ...fresh };
|
|
176
|
+
delete cache.sets[setId];
|
|
177
|
+
cache.sets[setId] = merged;
|
|
178
|
+
for (const key of Object.keys(cache.sets).slice(0, Math.max(0, Object.keys(cache.sets).length - MAX_CACHED_SETS)))
|
|
179
|
+
delete cache.sets[key];
|
|
180
|
+
try {
|
|
181
|
+
await fs.mkdir(path.dirname(this.cacheFile), { recursive: true });
|
|
182
|
+
await atomicWriteJson(this.cacheFile, cache);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// the cache is an optimisation
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Ask the model about tickets nothing matched.
|
|
190
|
+
*
|
|
191
|
+
* @returns Ticket → category (only tickets the model answered validly)
|
|
192
|
+
*/
|
|
193
|
+
async askModel(units, defs) {
|
|
194
|
+
const out = new Map();
|
|
195
|
+
if (!this.llm || units.length === 0)
|
|
196
|
+
return out;
|
|
197
|
+
const names = new Map(defs.map((d) => [d.name.toLowerCase(), d.name]));
|
|
198
|
+
const catalogue = defs.map((d) => `- ${d.name}${d.description ? `: ${d.description}` : ''}`).join('\n');
|
|
199
|
+
for (let call = 0; call < LLM_MAX_CALLS; call++) {
|
|
200
|
+
const batch = units.slice(call * LLM_BATCH, (call + 1) * LLM_BATCH);
|
|
201
|
+
if (batch.length === 0)
|
|
202
|
+
break;
|
|
203
|
+
const list = batch
|
|
204
|
+
.map((u, i) => `${i + 1}. ${[u.title, u.excerpt && u.excerpt !== u.title ? u.excerpt.slice(0, 160) : '', u.team ? `(team ${u.team})` : ''].filter(Boolean).join(' | ')}`)
|
|
205
|
+
.join('\n');
|
|
206
|
+
const prompt = `Assign each work item to exactly one category, or "${OTHER_CATEGORY}" if none fits.\n\nCategories:\n${catalogue}\n\nWork items:\n${list}\n\n` +
|
|
207
|
+
`Reply with only a JSON object mapping the item number to the category name, for example {"1":"${defs[0].name}","2":"${OTHER_CATEGORY}"}.`;
|
|
208
|
+
let reply = null;
|
|
209
|
+
try {
|
|
210
|
+
reply = await this.llm(prompt);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
reply = null;
|
|
214
|
+
}
|
|
215
|
+
if (!reply)
|
|
216
|
+
break;
|
|
217
|
+
const json = /\{[\s\S]*\}/.exec(reply)?.[0];
|
|
218
|
+
if (!json)
|
|
219
|
+
continue;
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(json);
|
|
222
|
+
batch.forEach((u, i) => {
|
|
223
|
+
const value = parsed[String(i + 1)];
|
|
224
|
+
if (typeof value !== 'string')
|
|
225
|
+
return;
|
|
226
|
+
const name = value.trim().toLowerCase();
|
|
227
|
+
if (name === OTHER_CATEGORY)
|
|
228
|
+
out.set(u.unitId, OTHER_CATEGORY);
|
|
229
|
+
else if (names.has(name))
|
|
230
|
+
out.set(u.unitId, names.get(name));
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// unparseable answer: those tickets stay unclassified
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The default cheap model: a small Anthropic model when `ANTHROPIC_API_KEY`
|
|
242
|
+
* is set; none otherwise.
|
|
243
|
+
*
|
|
244
|
+
* @returns A question function, or null
|
|
245
|
+
*/
|
|
246
|
+
export function defaultCategoryLlm() {
|
|
247
|
+
if (!process.env.ANTHROPIC_API_KEY)
|
|
248
|
+
return null;
|
|
249
|
+
return async (prompt) => {
|
|
250
|
+
const { generateText } = await import('ai');
|
|
251
|
+
const { anthropic } = await import('@ai-sdk/anthropic');
|
|
252
|
+
const controller = new AbortController();
|
|
253
|
+
const timer = setTimeout(() => controller.abort(), 30_000);
|
|
254
|
+
try {
|
|
255
|
+
const result = await generateText({ model: anthropic('claude-haiku-4-5'), prompt, abortSignal: controller.signal, maxOutputTokens: 600 });
|
|
256
|
+
return result.text;
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=unit-classifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unit-classifier.js","sourceRoot":"","sources":["../../../../../../backend/src/services/usage-ledger/unit-classifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The owner's own categories for usage reports ("self-media", "book", …).
|
|
3
|
+
*
|
|
4
|
+
* Categories are optional and never written into ticket facts: they only
|
|
5
|
+
* decide how a report groups tickets. Each definition has a natural-language
|
|
6
|
+
* description and optional hints (keywords, team names, project names). The
|
|
7
|
+
* set carries a version that changes whenever a definition changes; the
|
|
8
|
+
* classification cache is keyed on it.
|
|
9
|
+
*
|
|
10
|
+
* @module services/usage-ledger/usage-categories.store
|
|
11
|
+
*/
|
|
12
|
+
/** File holding the definitions. */
|
|
13
|
+
export declare const CATEGORIES_FILE = "categories.json";
|
|
14
|
+
/** One user-defined category. */
|
|
15
|
+
export interface CategoryDef {
|
|
16
|
+
/** Short label shown in reports */
|
|
17
|
+
name: string;
|
|
18
|
+
/** What belongs here, in plain words (also read by the language model) */
|
|
19
|
+
description?: string;
|
|
20
|
+
/** Words or phrases that put a ticket here (case-insensitive) */
|
|
21
|
+
keywords?: string[];
|
|
22
|
+
/** Team names that belong here */
|
|
23
|
+
teams?: string[];
|
|
24
|
+
/** Project names that belong here */
|
|
25
|
+
projects?: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validate and normalise a definition.
|
|
29
|
+
*
|
|
30
|
+
* @param input - Candidate
|
|
31
|
+
* @returns The definition, or an error text
|
|
32
|
+
*/
|
|
33
|
+
export declare function normalizeCategory(input: unknown): CategoryDef | string;
|
|
34
|
+
/**
|
|
35
|
+
* Identity of a set of definitions: the same definitions always give the same
|
|
36
|
+
* id, so a classification cache survives a set being re-entered unchanged.
|
|
37
|
+
*
|
|
38
|
+
* @param defs - Definitions
|
|
39
|
+
* @returns `default` for none, else a short hash
|
|
40
|
+
*/
|
|
41
|
+
export declare function categorySetId(defs: CategoryDef[]): string;
|
|
42
|
+
/**
|
|
43
|
+
* File-backed category definitions.
|
|
44
|
+
*/
|
|
45
|
+
export declare class UsageCategoriesStore {
|
|
46
|
+
readonly dir: string;
|
|
47
|
+
private readonly file;
|
|
48
|
+
/**
|
|
49
|
+
* @param dir - Ledger directory (default `~/.crewly/usage-ledger`)
|
|
50
|
+
*/
|
|
51
|
+
constructor(dir?: string);
|
|
52
|
+
private read;
|
|
53
|
+
private write;
|
|
54
|
+
/**
|
|
55
|
+
* The current definitions.
|
|
56
|
+
*
|
|
57
|
+
* @returns Version, set id and definitions
|
|
58
|
+
*/
|
|
59
|
+
list(): Promise<{
|
|
60
|
+
version: number;
|
|
61
|
+
setId: string;
|
|
62
|
+
updatedAt: string;
|
|
63
|
+
categories: CategoryDef[];
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* Add or replace one category (matched by name, case-insensitive).
|
|
67
|
+
*
|
|
68
|
+
* @param input - The definition
|
|
69
|
+
* @returns The new state, or an error text
|
|
70
|
+
*/
|
|
71
|
+
set(input: unknown): Promise<{
|
|
72
|
+
version: number;
|
|
73
|
+
categories: CategoryDef[];
|
|
74
|
+
} | string>;
|
|
75
|
+
/**
|
|
76
|
+
* Replace every definition.
|
|
77
|
+
*
|
|
78
|
+
* @param inputs - The new set (empty clears it)
|
|
79
|
+
* @returns The new state, or an error text
|
|
80
|
+
*/
|
|
81
|
+
replaceAll(inputs: unknown[]): Promise<{
|
|
82
|
+
version: number;
|
|
83
|
+
categories: CategoryDef[];
|
|
84
|
+
} | string>;
|
|
85
|
+
/**
|
|
86
|
+
* Remove one category.
|
|
87
|
+
*
|
|
88
|
+
* @param name - Its name (case-insensitive)
|
|
89
|
+
* @returns The new state, or null when there was no such category
|
|
90
|
+
*/
|
|
91
|
+
remove(name: string): Promise<{
|
|
92
|
+
version: number;
|
|
93
|
+
categories: CategoryDef[];
|
|
94
|
+
} | null>;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=usage-categories.store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-categories.store.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/usage-ledger/usage-categories.store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AASH,oCAAoC;AACpC,eAAO,MAAM,eAAe,oBAAoB,CAAC;AAEjD,iCAAiC;AACjC,MAAM,WAAW,WAAW;IAC3B,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AA0BD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,MAAM,CAiBtE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,CAIzD;AAED;;GAEG;AACH,qBAAa,oBAAoB;IAMpB,QAAQ,CAAC,GAAG,EAAE,MAAM;IALhC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAE9B;;OAEG;gBACkB,GAAG,GAAE,MAAwD;YAIpE,IAAI;YAYJ,KAAK;IAOnB;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,WAAW,EAAE,CAAA;KAAE,CAAC;IAKvG;;;;;OAKG;IACG,GAAG,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,WAAW,EAAE,CAAA;KAAE,GAAG,MAAM,CAAC;IAU3F;;;;;OAKG;IACG,UAAU,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,WAAW,EAAE,CAAA;KAAE,GAAG,MAAM,CAAC;IAYrG;;;;;OAKG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,WAAW,EAAE,CAAA;KAAE,GAAG,IAAI,CAAC;CAO1F"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The owner's own categories for usage reports ("self-media", "book", …).
|
|
3
|
+
*
|
|
4
|
+
* Categories are optional and never written into ticket facts: they only
|
|
5
|
+
* decide how a report groups tickets. Each definition has a natural-language
|
|
6
|
+
* description and optional hints (keywords, team names, project names). The
|
|
7
|
+
* set carries a version that changes whenever a definition changes; the
|
|
8
|
+
* classification cache is keyed on it.
|
|
9
|
+
*
|
|
10
|
+
* @module services/usage-ledger/usage-categories.store
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from 'crypto';
|
|
13
|
+
import { promises as fs } from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import { atomicWriteJson } from '../../utils/file-io.utils.js';
|
|
16
|
+
import { getCrewlyHomePath } from '../core/crewly-home.utils.js';
|
|
17
|
+
import { LEDGER_DIR_NAME } from './usage-ledger.store.js';
|
|
18
|
+
/** File holding the definitions. */
|
|
19
|
+
export const CATEGORIES_FILE = 'categories.json';
|
|
20
|
+
/** Longest name / description / hint accepted. */
|
|
21
|
+
const MAX_TEXT = 400;
|
|
22
|
+
const MAX_HINTS = 60;
|
|
23
|
+
const MAX_CATEGORIES = 40;
|
|
24
|
+
/**
|
|
25
|
+
* Clean a list of hints.
|
|
26
|
+
*
|
|
27
|
+
* @param value - Candidate list
|
|
28
|
+
* @returns Trimmed, de-duplicated strings (empty list becomes undefined)
|
|
29
|
+
*/
|
|
30
|
+
function cleanList(value) {
|
|
31
|
+
if (!Array.isArray(value))
|
|
32
|
+
return undefined;
|
|
33
|
+
const out = [...new Set(value.filter((v) => typeof v === 'string').map((v) => v.trim().slice(0, 120)).filter(Boolean))];
|
|
34
|
+
return out.length > 0 ? out.slice(0, MAX_HINTS) : undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validate and normalise a definition.
|
|
38
|
+
*
|
|
39
|
+
* @param input - Candidate
|
|
40
|
+
* @returns The definition, or an error text
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeCategory(input) {
|
|
43
|
+
if (!input || typeof input !== 'object')
|
|
44
|
+
return 'category must be an object';
|
|
45
|
+
const raw = input;
|
|
46
|
+
const name = typeof raw.name === 'string' ? raw.name.trim().slice(0, 60) : '';
|
|
47
|
+
if (!name)
|
|
48
|
+
return 'category needs a name';
|
|
49
|
+
if (name.toLowerCase() === 'other')
|
|
50
|
+
return '"other" is reserved for tickets that match nothing';
|
|
51
|
+
const description = typeof raw.description === 'string' && raw.description.trim() ? raw.description.trim().slice(0, MAX_TEXT) : undefined;
|
|
52
|
+
const keywords = cleanList(raw.keywords);
|
|
53
|
+
const teams = cleanList(raw.teams);
|
|
54
|
+
const projects = cleanList(raw.projects);
|
|
55
|
+
return {
|
|
56
|
+
name,
|
|
57
|
+
...(description ? { description } : {}),
|
|
58
|
+
...(keywords ? { keywords } : {}),
|
|
59
|
+
...(teams ? { teams } : {}),
|
|
60
|
+
...(projects ? { projects } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Identity of a set of definitions: the same definitions always give the same
|
|
65
|
+
* id, so a classification cache survives a set being re-entered unchanged.
|
|
66
|
+
*
|
|
67
|
+
* @param defs - Definitions
|
|
68
|
+
* @returns `default` for none, else a short hash
|
|
69
|
+
*/
|
|
70
|
+
export function categorySetId(defs) {
|
|
71
|
+
if (defs.length === 0)
|
|
72
|
+
return 'default';
|
|
73
|
+
const canonical = defs.map((d) => [d.name.toLowerCase(), d.description ?? '', d.keywords ?? [], d.teams ?? [], d.projects ?? []]);
|
|
74
|
+
return createHash('sha1').update(JSON.stringify(canonical)).digest('hex').slice(0, 10);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* File-backed category definitions.
|
|
78
|
+
*/
|
|
79
|
+
export class UsageCategoriesStore {
|
|
80
|
+
dir;
|
|
81
|
+
file;
|
|
82
|
+
/**
|
|
83
|
+
* @param dir - Ledger directory (default `~/.crewly/usage-ledger`)
|
|
84
|
+
*/
|
|
85
|
+
constructor(dir = path.join(getCrewlyHomePath(), LEDGER_DIR_NAME)) {
|
|
86
|
+
this.dir = dir;
|
|
87
|
+
this.file = path.join(dir, CATEGORIES_FILE);
|
|
88
|
+
}
|
|
89
|
+
async read() {
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(await fs.readFile(this.file, 'utf-8'));
|
|
92
|
+
const categories = (Array.isArray(parsed.categories) ? parsed.categories : [])
|
|
93
|
+
.map(normalizeCategory)
|
|
94
|
+
.filter((c) => typeof c !== 'string');
|
|
95
|
+
return { version: Number(parsed.version) || 0, updatedAt: parsed.updatedAt ?? '', categories };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { version: 0, updatedAt: '', categories: [] };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async write(categories, previousVersion) {
|
|
102
|
+
const next = { version: previousVersion + 1, updatedAt: new Date().toISOString(), categories };
|
|
103
|
+
await fs.mkdir(this.dir, { recursive: true });
|
|
104
|
+
await atomicWriteJson(this.file, next);
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The current definitions.
|
|
109
|
+
*
|
|
110
|
+
* @returns Version, set id and definitions
|
|
111
|
+
*/
|
|
112
|
+
async list() {
|
|
113
|
+
const file = await this.read();
|
|
114
|
+
return { version: file.version, setId: categorySetId(file.categories), updatedAt: file.updatedAt, categories: file.categories };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Add or replace one category (matched by name, case-insensitive).
|
|
118
|
+
*
|
|
119
|
+
* @param input - The definition
|
|
120
|
+
* @returns The new state, or an error text
|
|
121
|
+
*/
|
|
122
|
+
async set(input) {
|
|
123
|
+
const def = normalizeCategory(input);
|
|
124
|
+
if (typeof def === 'string')
|
|
125
|
+
return def;
|
|
126
|
+
const file = await this.read();
|
|
127
|
+
const rest = file.categories.filter((c) => c.name.toLowerCase() !== def.name.toLowerCase());
|
|
128
|
+
if (rest.length >= MAX_CATEGORIES)
|
|
129
|
+
return `at most ${MAX_CATEGORIES} categories`;
|
|
130
|
+
const written = await this.write([...rest, def], file.version);
|
|
131
|
+
return { version: written.version, categories: written.categories };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Replace every definition.
|
|
135
|
+
*
|
|
136
|
+
* @param inputs - The new set (empty clears it)
|
|
137
|
+
* @returns The new state, or an error text
|
|
138
|
+
*/
|
|
139
|
+
async replaceAll(inputs) {
|
|
140
|
+
const defs = [];
|
|
141
|
+
for (const input of inputs.slice(0, MAX_CATEGORIES)) {
|
|
142
|
+
const def = normalizeCategory(input);
|
|
143
|
+
if (typeof def === 'string')
|
|
144
|
+
return def;
|
|
145
|
+
defs.push(def);
|
|
146
|
+
}
|
|
147
|
+
const file = await this.read();
|
|
148
|
+
const written = await this.write(defs, file.version);
|
|
149
|
+
return { version: written.version, categories: written.categories };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Remove one category.
|
|
153
|
+
*
|
|
154
|
+
* @param name - Its name (case-insensitive)
|
|
155
|
+
* @returns The new state, or null when there was no such category
|
|
156
|
+
*/
|
|
157
|
+
async remove(name) {
|
|
158
|
+
const file = await this.read();
|
|
159
|
+
const rest = file.categories.filter((c) => c.name.toLowerCase() !== name.trim().toLowerCase());
|
|
160
|
+
if (rest.length === file.categories.length)
|
|
161
|
+
return null;
|
|
162
|
+
const written = await this.write(rest, file.version);
|
|
163
|
+
return { version: written.version, categories: written.categories };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=usage-categories.store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-categories.store.js","sourceRoot":"","sources":["../../../../../../backend/src/services/usage-ledger/usage-categories.store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time rebuild of the ledger for the time before it existed.
|
|
3
|
+
*
|
|
4
|
+
* Sources: the token ledger (usage events) and the run traces (ticket roots,
|
|
5
|
+
* deliveries, busy turns, status changes). The traces expire after 90 days,
|
|
6
|
+
* the ledger does not, so this keeps what they know.
|
|
7
|
+
*
|
|
8
|
+
* Attribution of a usage event:
|
|
9
|
+
* - it already carries a trace id (the harness stamped it): `recorded`;
|
|
10
|
+
* - otherwise it goes to the trace the session's latest delivery named,
|
|
11
|
+
* when that delivery was at most {@link INFER_MAX_GAP_MS} before the event:
|
|
12
|
+
* `inferred`;
|
|
13
|
+
* - otherwise it lands in the agent's "(no ticket)" unit.
|
|
14
|
+
*
|
|
15
|
+
* Only facts older than the cutoff (the moment live recording began) are
|
|
16
|
+
* written, so history and live facts never overlap. The result goes to
|
|
17
|
+
* `backfill.jsonl`; the run is recorded in `state.json` and never repeats.
|
|
18
|
+
*
|
|
19
|
+
* @module services/usage-ledger/usage-ledger.backfill
|
|
20
|
+
*/
|
|
21
|
+
import { type TokenUsageEvent } from '../monitoring/token-usage.service.js';
|
|
22
|
+
import type { TeamLookup } from './team-directory.js';
|
|
23
|
+
import type { UsageLedgerStore } from './usage-ledger.store.js';
|
|
24
|
+
/** An unticketed event this soon after a delivery is credited to the delivery's trace. */
|
|
25
|
+
export declare const INFER_MAX_GAP_MS: number;
|
|
26
|
+
/** File with the ledger's own bookkeeping. */
|
|
27
|
+
export declare const STATE_FILE = "state.json";
|
|
28
|
+
/** Bookkeeping persisted beside the facts. */
|
|
29
|
+
export interface LedgerState {
|
|
30
|
+
/** When live recording began (the backfill cutoff) */
|
|
31
|
+
liveSince?: string;
|
|
32
|
+
backfill?: {
|
|
33
|
+
doneAt: string;
|
|
34
|
+
cutoff: string;
|
|
35
|
+
facts: number;
|
|
36
|
+
usageEvents: number;
|
|
37
|
+
inferredEvents: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Where history comes from. */
|
|
41
|
+
export interface BackfillSources {
|
|
42
|
+
/** Usage events of every session */
|
|
43
|
+
forEachUsage: (visit: (session: string, event: TokenUsageEvent) => void) => void;
|
|
44
|
+
/** The traces directory (index.json + `<id>.jsonl`) */
|
|
45
|
+
traceDir: string;
|
|
46
|
+
directory: TeamLookup;
|
|
47
|
+
}
|
|
48
|
+
/** What a backfill did. */
|
|
49
|
+
export interface BackfillResult {
|
|
50
|
+
ran: boolean;
|
|
51
|
+
reason?: string;
|
|
52
|
+
facts: number;
|
|
53
|
+
usageEvents: number;
|
|
54
|
+
inferredEvents: number;
|
|
55
|
+
traces: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Read the ledger bookkeeping.
|
|
59
|
+
*
|
|
60
|
+
* @param dir - Ledger directory
|
|
61
|
+
* @returns State (empty when none)
|
|
62
|
+
*/
|
|
63
|
+
export declare function readLedgerState(dir: string): Promise<LedgerState>;
|
|
64
|
+
/**
|
|
65
|
+
* Write the ledger bookkeeping.
|
|
66
|
+
*
|
|
67
|
+
* @param dir - Ledger directory
|
|
68
|
+
* @param state - State
|
|
69
|
+
*/
|
|
70
|
+
export declare function writeLedgerState(dir: string, state: LedgerState): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Note when live recording began (once).
|
|
73
|
+
*
|
|
74
|
+
* @param dir - Ledger directory
|
|
75
|
+
* @param now - Now (ms)
|
|
76
|
+
* @returns The cutoff
|
|
77
|
+
*/
|
|
78
|
+
export declare function ensureLiveSince(dir: string, now?: number): Promise<string>;
|
|
79
|
+
/**
|
|
80
|
+
* Rebuild history into `backfill.jsonl`.
|
|
81
|
+
*
|
|
82
|
+
* @param store - Ledger store
|
|
83
|
+
* @param sources - Where history comes from
|
|
84
|
+
* @param opts.cutoffMs - Only facts before this time are written
|
|
85
|
+
* @returns What happened
|
|
86
|
+
*/
|
|
87
|
+
export declare function runBackfill(store: UsageLedgerStore, sources: BackfillSources, opts: {
|
|
88
|
+
cutoffMs: number;
|
|
89
|
+
}): Promise<BackfillResult>;
|
|
90
|
+
//# sourceMappingURL=usage-ledger.backfill.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-ledger.backfill.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/usage-ledger/usage-ledger.backfill.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAOH,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,sCAAsC,CAAC;AAGvG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,0FAA0F;AAC1F,eAAO,MAAM,gBAAgB,QAAgC,CAAC;AAE9D,8CAA8C;AAC9C,eAAO,MAAM,UAAU,eAAe,CAAC;AAEvC,8CAA8C;AAC9C,MAAM,WAAW,WAAW;IAC3B,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1G;AAED,gCAAgC;AAChC,MAAM,WAAW,eAAe;IAC/B,oCAAoC;IACpC,YAAY,EAAE,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,KAAK,IAAI,KAAK,IAAI,CAAC;IACjF,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,UAAU,CAAC;CACtB;AAED,2BAA2B;AAC3B,MAAM,WAAW,cAAc;IAC9B,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAMvE;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAGrF;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAM5F;AAUD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAsMxI"}
|