@knpkv/jira-clockify 1.4.1 → 1.6.0
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/README.md +105 -22
- package/dist/package.json +1 -1
- package/dist/src/agent/schedule.d.ts +6 -5
- package/dist/src/agent/schedule.d.ts.map +1 -1
- package/dist/src/agent/schedule.js +117 -102
- package/dist/src/agent/schedule.js.map +1 -1
- package/dist/src/agent/sessions.d.ts +27 -33
- package/dist/src/agent/sessions.d.ts.map +1 -1
- package/dist/src/agent/sessions.js +121 -83
- package/dist/src/agent/sessions.js.map +1 -1
- package/dist/src/agent/writePlanning.d.ts +2 -0
- package/dist/src/agent/writePlanning.d.ts.map +1 -1
- package/dist/src/agent/writePlanning.js +4 -1
- package/dist/src/agent/writePlanning.js.map +1 -1
- package/dist/src/bin.js +16 -5
- package/dist/src/bin.js.map +1 -1
- package/dist/src/cli/CommandFailed.d.ts +27 -0
- package/dist/src/cli/CommandFailed.d.ts.map +1 -0
- package/dist/src/cli/CommandFailed.js +25 -0
- package/dist/src/cli/CommandFailed.js.map +1 -0
- package/dist/src/cli/agentWrite.d.ts.map +1 -1
- package/dist/src/cli/agentWrite.js +12 -5
- package/dist/src/cli/agentWrite.js.map +1 -1
- package/dist/src/cli/auth.d.ts +59 -2
- package/dist/src/cli/auth.d.ts.map +1 -1
- package/dist/src/cli/auth.js +151 -106
- package/dist/src/cli/auth.js.map +1 -1
- package/dist/src/cli/config.d.ts +4 -3
- package/dist/src/cli/config.d.ts.map +1 -1
- package/dist/src/cli/config.js +73 -31
- package/dist/src/cli/config.js.map +1 -1
- package/dist/src/cli/fetchTicket.d.ts +3 -3
- package/dist/src/cli/fetchTicket.d.ts.map +1 -1
- package/dist/src/cli/fetchTicket.js +7 -7
- package/dist/src/cli/fetchTicket.js.map +1 -1
- package/dist/src/cli/layers.d.ts +6 -5
- package/dist/src/cli/layers.d.ts.map +1 -1
- package/dist/src/cli/layers.js +16 -15
- package/dist/src/cli/layers.js.map +1 -1
- package/dist/src/cli/list.d.ts +3 -2
- package/dist/src/cli/list.d.ts.map +1 -1
- package/dist/src/cli/list.js +11 -5
- package/dist/src/cli/list.js.map +1 -1
- package/dist/src/cli/reconcile.d.ts +18 -3
- package/dist/src/cli/reconcile.d.ts.map +1 -1
- package/dist/src/cli/reconcile.js +73 -23
- package/dist/src/cli/reconcile.js.map +1 -1
- package/dist/src/cli/root.d.ts.map +1 -1
- package/dist/src/cli/root.js +3 -3
- package/dist/src/cli/root.js.map +1 -1
- package/dist/src/cli/setup.d.ts +14 -4
- package/dist/src/cli/setup.d.ts.map +1 -1
- package/dist/src/cli/setup.js +45 -94
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/timer/discard.d.ts +1 -1
- package/dist/src/cli/timer/discard.d.ts.map +1 -1
- package/dist/src/cli/timer/discard.js +2 -1
- package/dist/src/cli/timer/discard.js.map +1 -1
- package/dist/src/cli/timer/log.d.ts +1 -1
- package/dist/src/cli/timer/log.d.ts.map +1 -1
- package/dist/src/cli/timer/log.js +2 -1
- package/dist/src/cli/timer/log.js.map +1 -1
- package/dist/src/cli/timer/start.d.ts +2 -1
- package/dist/src/cli/timer/start.d.ts.map +1 -1
- package/dist/src/cli/timer/start.js +6 -8
- package/dist/src/cli/timer/start.js.map +1 -1
- package/dist/src/cli/timer/stop.d.ts +1 -1
- package/dist/src/cli/timer/stop.d.ts.map +1 -1
- package/dist/src/cli/timer.d.ts.map +1 -1
- package/dist/src/cli/timer.js +8 -1
- package/dist/src/cli/timer.js.map +1 -1
- package/dist/src/cli/unknownCommand.d.ts +24 -0
- package/dist/src/cli/unknownCommand.d.ts.map +1 -0
- package/dist/src/cli/unknownCommand.js +84 -0
- package/dist/src/cli/unknownCommand.js.map +1 -0
- package/dist/src/cli/watch.d.ts +2 -2
- package/dist/src/cli/watch.d.ts.map +1 -1
- package/dist/src/services/AgentSessionReader.d.ts +26 -12
- package/dist/src/services/AgentSessionReader.d.ts.map +1 -1
- package/dist/src/services/AgentSessionReader.js +163 -69
- package/dist/src/services/AgentSessionReader.js.map +1 -1
- package/dist/src/services/CodexTranscript.d.ts +50 -13
- package/dist/src/services/CodexTranscript.d.ts.map +1 -1
- package/dist/src/services/CodexTranscript.js +49 -13
- package/dist/src/services/CodexTranscript.js.map +1 -1
- package/dist/src/services/ConfigService.d.ts +4 -0
- package/dist/src/services/ConfigService.d.ts.map +1 -1
- package/dist/src/services/ConfigService.js +6 -2
- package/dist/src/services/ConfigService.js.map +1 -1
- package/dist/src/services/IssueFacts.d.ts +2 -2
- package/dist/src/services/IssueFacts.d.ts.map +1 -1
- package/dist/src/services/IssueFacts.js +5 -5
- package/dist/src/services/IssueFacts.js.map +1 -1
- package/dist/src/services/JiraAccess.d.ts +140 -0
- package/dist/src/services/JiraAccess.d.ts.map +1 -0
- package/dist/src/services/JiraAccess.js +204 -0
- package/dist/src/services/JiraAccess.js.map +1 -0
- package/dist/src/services/ProviderSnapshots.d.ts +41 -0
- package/dist/src/services/ProviderSnapshots.d.ts.map +1 -0
- package/dist/src/services/ProviderSnapshots.js +71 -0
- package/dist/src/services/ProviderSnapshots.js.map +1 -0
- package/dist/src/services/ReconcileService.d.ts +12 -4
- package/dist/src/services/ReconcileService.d.ts.map +1 -1
- package/dist/src/services/ReconcileService.js +223 -113
- package/dist/src/services/ReconcileService.js.map +1 -1
- package/dist/src/services/SavedEntries.d.ts +24 -4
- package/dist/src/services/SavedEntries.d.ts.map +1 -1
- package/dist/src/services/SavedEntries.js +335 -19
- package/dist/src/services/SavedEntries.js.map +1 -1
- package/dist/src/services/SessionAttributor.d.ts +12 -0
- package/dist/src/services/SessionAttributor.d.ts.map +1 -1
- package/dist/src/services/SessionAttributor.js +13 -1
- package/dist/src/services/SessionAttributor.js.map +1 -1
- package/dist/src/services/SessionLine.d.ts +29 -0
- package/dist/src/services/SessionLine.d.ts.map +1 -0
- package/dist/src/services/SessionLine.js +8 -0
- package/dist/src/services/SessionLine.js.map +1 -0
- package/dist/src/services/SourceLedger.d.ts +84 -1
- package/dist/src/services/SourceLedger.d.ts.map +1 -1
- package/dist/src/services/SourceLedger.js +178 -12
- package/dist/src/services/SourceLedger.js.map +1 -1
- package/dist/src/services/TicketRanking.d.ts +39 -0
- package/dist/src/services/TicketRanking.d.ts.map +1 -0
- package/dist/src/services/TicketRanking.js +87 -0
- package/dist/src/services/TicketRanking.js.map +1 -0
- package/dist/src/services/TimerService.d.ts +3 -3
- package/dist/src/services/TimerService.d.ts.map +1 -1
- package/dist/src/services/TimerService.js +6 -17
- package/dist/src/services/TimerService.js.map +1 -1
- package/dist/src/services/internal/retryTransport.d.ts +9 -0
- package/dist/src/services/internal/retryTransport.d.ts.map +1 -0
- package/dist/src/services/internal/retryTransport.js +15 -0
- package/dist/src/services/internal/retryTransport.js.map +1 -0
- package/dist/src/testing/fakeHeadless.d.ts +12 -1
- package/dist/src/testing/fakeHeadless.d.ts.map +1 -1
- package/dist/src/testing/fakeHeadless.js +83 -10
- package/dist/src/testing/fakeHeadless.js.map +1 -1
- package/dist/src/tui/App.js +1 -1
- package/dist/src/tui/atoms/runtime.d.ts +1 -1
- package/dist/src/tui/atoms/runtime.d.ts.map +1 -1
- package/dist/src/utils/hints.d.ts +4 -2
- package/dist/src/utils/hints.d.ts.map +1 -1
- package/dist/src/utils/hints.js +4 -2
- package/dist/src/utils/hints.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/skills/jcf/SKILL.md +9 -6
|
@@ -3,7 +3,7 @@ import * as SourceConsumption from "./sourceConsumption.js";
|
|
|
3
3
|
* Which evidence placed a session on an Issue Key. `none` means no signal did — the session is
|
|
4
4
|
* an Unattributed Session and is reported with its hours rather than guessed at.
|
|
5
5
|
*/
|
|
6
|
-
export type AttributionSignal = "branch" | "path" | "standing" | "agent" | "none";
|
|
6
|
+
export type AttributionSignal = "branch" | "path" | "standing" | "agent" | "split" | "none";
|
|
7
7
|
/**
|
|
8
8
|
* Attribution Signals in precedence order — the first to yield an Issue Key wins. Also the
|
|
9
9
|
* trust order: a bucket built from several sessions reports its *weakest* signal, because a row
|
|
@@ -19,6 +19,8 @@ export interface AttributableSession {
|
|
|
19
19
|
readonly gitBranch: string | null;
|
|
20
20
|
/** Issue Keys mined from the transcript — the closed choice set for a Coding Agent. */
|
|
21
21
|
readonly candidateKeys: ReadonlyArray<string>;
|
|
22
|
+
/** Counts from the full in-scope transcript, before its bounded agent digest is constructed. */
|
|
23
|
+
readonly mentionCounts?: ReadonlyMap<string, number>;
|
|
22
24
|
}
|
|
23
25
|
/** How one Agent Session was placed (or not placed) on an Issue Key. */
|
|
24
26
|
export interface SessionAttribution {
|
|
@@ -32,7 +34,16 @@ export interface SessionAttribution {
|
|
|
32
34
|
* Such credit is reported so the hours stay visible, but is never offered for confirmation.
|
|
33
35
|
*/
|
|
34
36
|
readonly belowConfidenceFloor: boolean;
|
|
37
|
+
/** Sprint candidates sharing this session, weighted by transcript mentions. Absent for single-ticket attribution. */
|
|
38
|
+
readonly targets?: ReadonlyArray<{
|
|
39
|
+
readonly ticketKey: string;
|
|
40
|
+
readonly weight: number;
|
|
41
|
+
}>;
|
|
35
42
|
}
|
|
43
|
+
/** Count transcript mentions without deduplicating repeated ticket keys. */
|
|
44
|
+
export declare const ticketMentionCounts: (text: string) => ReadonlyMap<string, number>;
|
|
45
|
+
/** Split only unplaced or low-confidence sessions onto evidenced tickets in my known open sprint. */
|
|
46
|
+
export declare const splitSessionAttribution: (session: AttributableSession, attribution: SessionAttribution, sprint: ReadonlySet<string> | undefined, ignored: ReadonlySet<string>) => SessionAttribution;
|
|
36
47
|
/**
|
|
37
48
|
* True when a string is an Issue Key and nothing else.
|
|
38
49
|
*
|
|
@@ -104,6 +115,7 @@ export interface DeterministicAttribution {
|
|
|
104
115
|
*/
|
|
105
116
|
export declare const deterministicAttribution: (session: AttributableSession, options: {
|
|
106
117
|
readonly standingMap: Readonly<Record<string, string>>;
|
|
118
|
+
readonly ignoredTickets?: ReadonlySet<string> | undefined;
|
|
107
119
|
}) => DeterministicAttribution | null;
|
|
108
120
|
/** What a Coding Agent decided about one session. `null` is a valid, expected answer. */
|
|
109
121
|
export interface AgentChoice {
|
|
@@ -120,6 +132,7 @@ export declare const attributeSession: (session: AttributableSession, options: {
|
|
|
120
132
|
/** Only consulted when no deterministic signal placed the session. */
|
|
121
133
|
readonly agentChoice?: AgentChoice | null | undefined;
|
|
122
134
|
readonly confidenceFloor: number;
|
|
135
|
+
readonly ignoredTickets?: ReadonlySet<string> | undefined;
|
|
123
136
|
}) => SessionAttribution;
|
|
124
137
|
/** One recorded moment of work inside an Agent Session. */
|
|
125
138
|
export interface SessionActivity {
|
|
@@ -211,41 +224,16 @@ export interface OwnedRun {
|
|
|
211
224
|
readonly startMs: number;
|
|
212
225
|
readonly endMs: number;
|
|
213
226
|
readonly bucketIds: ReadonlyArray<string>;
|
|
227
|
+
readonly weights?: ReadonlyMap<string, number> | undefined;
|
|
214
228
|
}
|
|
215
229
|
/**
|
|
216
|
-
* Coalesce ownership
|
|
230
|
+
* Coalesce rapid ownership changes without removing overlapping tickets.
|
|
217
231
|
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* **Two rules, in order.**
|
|
224
|
-
*
|
|
225
|
-
* 1. *A ticket present for less than the floor inside one connected source cluster, interrupting work
|
|
226
|
-
* that resumes after it, was not a ticket that was worked on.* It was a keystroke inside other work — a branch
|
|
227
|
-
* checked, a file opened, a question asked — so its time goes to the work around it. This is what
|
|
228
|
-
* actually removes the slivers: they rarely touch anything, because a transcript goes quiet
|
|
229
|
-
* between prompts, and they are as often overlapping as adjacent, because concurrent sessions
|
|
230
|
-
* overlap by definition. Adjacency and overlap were both tried as the test and both left the
|
|
231
|
-
* interleaving as they found it. The requirement for work on *both* sides is what keeps a genuine
|
|
232
|
-
* short piece of work — eight minutes on another ticket, and then the day moves on — from being
|
|
233
|
-
* swallowed by what came before it.
|
|
234
|
-
* 2. *Tenancy, not adjacency.* Between tickets that were genuinely worked on, whoever takes the
|
|
235
|
-
* timeline holds it for at least the floor: a stretch beginning inside that tenure and owned by
|
|
236
|
-
* someone else is credited to the incumbent. This catches two real tickets alternating quickly.
|
|
237
|
-
*
|
|
238
|
-
* **What it never does.** It reassigns time and never creates or drops any — idle gaps are not swept
|
|
239
|
-
* into a tenure — so the inequality that makes a proposal safe to accept survives intact. Nothing is
|
|
240
|
-
* ever welded across a local midnight, because runs are bucketed by the day they start in.
|
|
241
|
-
*
|
|
242
|
-
* **Unplaced hours take no part.** A stretch nothing placed neither holds a tenure nor loses its time
|
|
243
|
-
* to one: promoting it would bill work no transcript placed on that ticket, and demoting an
|
|
244
|
-
* attributed sliver into it would quietly discard billable work. An attributed tenure simply
|
|
245
|
-
* continues across it.
|
|
246
|
-
*
|
|
247
|
-
* **A minor ticket with nothing to belong to keeps its own time.** Four minutes alone in an otherwise
|
|
248
|
-
* empty day has no surrounding work to join, and dropping it would lose work that happened.
|
|
232
|
+
* A short sole-owner interruption returns to surrounding major work only when that work resumes
|
|
233
|
+
* in the same connected source cluster. Overlapping tickets keep their ownership regardless of
|
|
234
|
+
* their presence length. Tenancy holds between disjoint attributed owners for the dwell period.
|
|
235
|
+
* Unplaced time retains its own credit and cannot acquire or lose an attributed ticket's time.
|
|
236
|
+
* Idle gaps and local midnight are barriers; no coalescing creates additional wall-clock time.
|
|
249
237
|
*/
|
|
250
238
|
export declare const applyDwellFloor: (slices: ReadonlyArray<OwnedRun>, options: {
|
|
251
239
|
readonly dwellSeconds: number;
|
|
@@ -324,6 +312,10 @@ export declare const splitCredits: (windows: ReadonlyArray<SessionWindows>, attr
|
|
|
324
312
|
* {@link DEFAULT_DWELL_SECONDS}; zero turns the rule off and reports the raw interleaving.
|
|
325
313
|
*/
|
|
326
314
|
readonly dwellSeconds?: number | undefined;
|
|
315
|
+
/** Higher values receive writable minutes first when a shared stretch is too short. */
|
|
316
|
+
readonly priority?: ((ticketKey: string, day: string) => number) | undefined;
|
|
317
|
+
/** Remove these tickets' windows before sharing; overlapping tickets absorb them, never solo time. */
|
|
318
|
+
readonly ignoredTickets?: ReadonlySet<string> | undefined;
|
|
327
319
|
}) => CreditSplit;
|
|
328
320
|
/** What Clockify and Jira already hold for one `(Issue Key, day)` bucket. */
|
|
329
321
|
export interface RecordedBucket {
|
|
@@ -346,6 +338,8 @@ export interface RecordedBucket {
|
|
|
346
338
|
}
|
|
347
339
|
/** A Proposed Worklog: the gap between what a session accounts for and what is already recorded. */
|
|
348
340
|
export interface SessionProposal {
|
|
341
|
+
/** Provider-window review holds. Evidence remains readable; the named providers cannot write it. */
|
|
342
|
+
readonly writeBlocked?: Readonly<Partial<Record<SourceConsumption.Source, string>>> | undefined;
|
|
349
343
|
readonly ticketKey: string;
|
|
350
344
|
readonly day: string;
|
|
351
345
|
readonly signal: AttributionSignal;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../../src/agent/sessions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAuC3D;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../../src/agent/sessions.ts"],"names":[],"mappings":"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"}
|
|
@@ -44,6 +44,7 @@ export const ATTRIBUTION_PRECEDENCE = [
|
|
|
44
44
|
"path",
|
|
45
45
|
"standing",
|
|
46
46
|
"agent",
|
|
47
|
+
"split",
|
|
47
48
|
"none"
|
|
48
49
|
];
|
|
49
50
|
const signalRank = (signal) => {
|
|
@@ -62,6 +63,34 @@ const signalRank = (signal) => {
|
|
|
62
63
|
*/
|
|
63
64
|
const TICKET_KEY = /(?<![A-Za-z0-9])[A-Z][A-Z0-9]{1,9}-\d{1,6}(?![A-Za-z0-9])/g;
|
|
64
65
|
const matchTicketKeys = (text) => [...text.matchAll(TICKET_KEY)].map((m) => m[0]);
|
|
66
|
+
/** Count transcript mentions without deduplicating repeated ticket keys. */
|
|
67
|
+
export const ticketMentionCounts = (text) => {
|
|
68
|
+
const counts = new Map();
|
|
69
|
+
for (const key of matchTicketKeys(text))
|
|
70
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
71
|
+
return counts;
|
|
72
|
+
};
|
|
73
|
+
/** Split only unplaced or low-confidence sessions onto evidenced tickets in my known open sprint. */
|
|
74
|
+
export const splitSessionAttribution = (session, attribution, sprint, ignored) => {
|
|
75
|
+
const counts = session.mentionCounts;
|
|
76
|
+
if (sprint === undefined || counts === undefined ||
|
|
77
|
+
(attribution.ticketKey !== null && !attribution.belowConfidenceFloor))
|
|
78
|
+
return attribution;
|
|
79
|
+
const targets = [...new Set(session.candidateKeys)]
|
|
80
|
+
.filter((key) => sprint.has(key) && !ignored.has(key))
|
|
81
|
+
.flatMap((ticketKey) => {
|
|
82
|
+
const weight = counts.get(ticketKey);
|
|
83
|
+
return weight === undefined ? [] : [{ ticketKey, weight }];
|
|
84
|
+
});
|
|
85
|
+
return targets.length === 0 ? attribution : {
|
|
86
|
+
sessionId: attribution.sessionId,
|
|
87
|
+
ticketKey: null,
|
|
88
|
+
signal: "split",
|
|
89
|
+
confidence: null,
|
|
90
|
+
belowConfidenceFloor: false,
|
|
91
|
+
targets
|
|
92
|
+
};
|
|
93
|
+
};
|
|
65
94
|
/**
|
|
66
95
|
* True when a string is an Issue Key and nothing else.
|
|
67
96
|
*
|
|
@@ -189,14 +218,17 @@ export const standingAttribution = (cwd, map) => {
|
|
|
189
218
|
*/
|
|
190
219
|
export const deterministicAttribution = (session, options) => {
|
|
191
220
|
const fromBranch = ticketKeyFromBranch(session.gitBranch);
|
|
192
|
-
if (fromBranch !== null)
|
|
193
|
-
return { ticketKey: fromBranch, signal: "branch" };
|
|
221
|
+
if (fromBranch !== null) {
|
|
222
|
+
return options.ignoredTickets?.has(fromBranch) === true ? null : { ticketKey: fromBranch, signal: "branch" };
|
|
223
|
+
}
|
|
194
224
|
const fromPath = ticketKeyFromPath(session.cwd);
|
|
195
|
-
if (fromPath !== null)
|
|
196
|
-
return { ticketKey: fromPath, signal: "path" };
|
|
225
|
+
if (fromPath !== null) {
|
|
226
|
+
return options.ignoredTickets?.has(fromPath) === true ? null : { ticketKey: fromPath, signal: "path" };
|
|
227
|
+
}
|
|
197
228
|
const standing = standingAttribution(session.cwd, options.standingMap);
|
|
198
|
-
if (standing !== null)
|
|
199
|
-
return { ticketKey: standing, signal: "standing" };
|
|
229
|
+
if (standing !== null) {
|
|
230
|
+
return options.ignoredTickets?.has(standing) === true ? null : { ticketKey: standing, signal: "standing" };
|
|
231
|
+
}
|
|
200
232
|
return null;
|
|
201
233
|
};
|
|
202
234
|
/**
|
|
@@ -205,7 +237,7 @@ export const deterministicAttribution = (session, options) => {
|
|
|
205
237
|
* choice below `confidenceFloor` is marked so it can be reported without being offered.
|
|
206
238
|
*/
|
|
207
239
|
export const attributeSession = (session, options) => {
|
|
208
|
-
const deterministic = deterministicAttribution(session,
|
|
240
|
+
const deterministic = deterministicAttribution(session, options);
|
|
209
241
|
if (deterministic !== null) {
|
|
210
242
|
return {
|
|
211
243
|
sessionId: session.sessionId,
|
|
@@ -225,7 +257,8 @@ export const attributeSession = (session, options) => {
|
|
|
225
257
|
};
|
|
226
258
|
// The choice set is closed over the transcript's own text: a key the transcript never
|
|
227
259
|
// mentioned cannot reach a worklog, however confidently it was named.
|
|
228
|
-
if (choice === null || choice === undefined ||
|
|
260
|
+
if (choice === null || choice === undefined || options.ignoredTickets?.has(choice.ticketKey) === true ||
|
|
261
|
+
!session.candidateKeys.includes(choice.ticketKey)) {
|
|
229
262
|
return unattributed;
|
|
230
263
|
}
|
|
231
264
|
return {
|
|
@@ -340,11 +373,15 @@ const overlapSlices = (spansByBucket) => {
|
|
|
340
373
|
for (let i = 0; i < ordered.length - 1; i++) {
|
|
341
374
|
const startMs = ordered[i];
|
|
342
375
|
const endMs = ordered[i + 1];
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
376
|
+
const weights = new Map([...spansByBucket].flatMap(([id, spans]) => {
|
|
377
|
+
const active = spans.filter((span) => span.startMs <= startMs && span.endMs >= endMs);
|
|
378
|
+
// Duplicate sessions on one ticket union their presence, rather than winning extra time.
|
|
379
|
+
return active.length === 0
|
|
380
|
+
? []
|
|
381
|
+
: [[id, Math.max(...active.map((span) => span.weight ?? 1))]];
|
|
382
|
+
}));
|
|
383
|
+
if (weights.size > 0)
|
|
384
|
+
slices.push({ startMs, endMs, bucketIds: [...weights.keys()], weights });
|
|
348
385
|
}
|
|
349
386
|
return slices;
|
|
350
387
|
};
|
|
@@ -367,6 +404,7 @@ const sameOwners = (a, b) => a.length === b.length && a.every((id, index) => id
|
|
|
367
404
|
*/
|
|
368
405
|
const joinable = (previous, next) => previous.endMs === next.startMs &&
|
|
369
406
|
sameOwners(previous.bucketIds, next.bucketIds) &&
|
|
407
|
+
previous.bucketIds.every((id) => (previous.weights?.get(id) ?? 1) === (next.weights?.get(id) ?? 1)) &&
|
|
370
408
|
localDay(new Date(previous.startMs)) === localDay(new Date(next.startMs));
|
|
371
409
|
/** Join touching stretches with identical owners. A gap of idle time always separates two runs. */
|
|
372
410
|
const mergeRuns = (runs) => {
|
|
@@ -382,39 +420,13 @@ const mergeRuns = (runs) => {
|
|
|
382
420
|
return merged;
|
|
383
421
|
};
|
|
384
422
|
/**
|
|
385
|
-
* Coalesce ownership
|
|
386
|
-
*
|
|
387
|
-
* **What this is for.** Three concurrent sessions on three Issue Keys interleave their prompts, and
|
|
388
|
-
* read literally that says the work changed ticket every few minutes. It did not — that is an
|
|
389
|
-
* artefact of reading several transcripts at once. A day of it is two dozen slivers on a calendar and
|
|
390
|
-
* indefensible on a timesheet.
|
|
423
|
+
* Coalesce rapid ownership changes without removing overlapping tickets.
|
|
391
424
|
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* actually removes the slivers: they rarely touch anything, because a transcript goes quiet
|
|
398
|
-
* between prompts, and they are as often overlapping as adjacent, because concurrent sessions
|
|
399
|
-
* overlap by definition. Adjacency and overlap were both tried as the test and both left the
|
|
400
|
-
* interleaving as they found it. The requirement for work on *both* sides is what keeps a genuine
|
|
401
|
-
* short piece of work — eight minutes on another ticket, and then the day moves on — from being
|
|
402
|
-
* swallowed by what came before it.
|
|
403
|
-
* 2. *Tenancy, not adjacency.* Between tickets that were genuinely worked on, whoever takes the
|
|
404
|
-
* timeline holds it for at least the floor: a stretch beginning inside that tenure and owned by
|
|
405
|
-
* someone else is credited to the incumbent. This catches two real tickets alternating quickly.
|
|
406
|
-
*
|
|
407
|
-
* **What it never does.** It reassigns time and never creates or drops any — idle gaps are not swept
|
|
408
|
-
* into a tenure — so the inequality that makes a proposal safe to accept survives intact. Nothing is
|
|
409
|
-
* ever welded across a local midnight, because runs are bucketed by the day they start in.
|
|
410
|
-
*
|
|
411
|
-
* **Unplaced hours take no part.** A stretch nothing placed neither holds a tenure nor loses its time
|
|
412
|
-
* to one: promoting it would bill work no transcript placed on that ticket, and demoting an
|
|
413
|
-
* attributed sliver into it would quietly discard billable work. An attributed tenure simply
|
|
414
|
-
* continues across it.
|
|
415
|
-
*
|
|
416
|
-
* **A minor ticket with nothing to belong to keeps its own time.** Four minutes alone in an otherwise
|
|
417
|
-
* empty day has no surrounding work to join, and dropping it would lose work that happened.
|
|
425
|
+
* A short sole-owner interruption returns to surrounding major work only when that work resumes
|
|
426
|
+
* in the same connected source cluster. Overlapping tickets keep their ownership regardless of
|
|
427
|
+
* their presence length. Tenancy holds between disjoint attributed owners for the dwell period.
|
|
428
|
+
* Unplaced time retains its own credit and cannot acquire or lose an attributed ticket's time.
|
|
429
|
+
* Idle gaps and local midnight are barriers; no coalescing creates additional wall-clock time.
|
|
418
430
|
*/
|
|
419
431
|
export const applyDwellFloor = (slices, options) => {
|
|
420
432
|
const dwellMs = Math.max(0, options.dwellSeconds) * 1000;
|
|
@@ -467,16 +479,15 @@ export const applyDwellFloor = (slices, options) => {
|
|
|
467
479
|
};
|
|
468
480
|
const withoutMinors = runs.map((run, index) => {
|
|
469
481
|
const day = localDay(new Date(run.startMs));
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
return { ...run, bucketIds: kept };
|
|
482
|
+
if (run.bucketIds.length > 1 || run.bucketIds.some((id) => major(id, index)))
|
|
483
|
+
return run;
|
|
473
484
|
// Every owner was minor, so this is only an *interruption* if the work it interrupts resumes:
|
|
474
485
|
// there has to be major work on both sides of it. Eight minutes on another ticket after half an
|
|
475
486
|
// hour, with nothing after it, is a short piece of work rather than a keystroke inside a longer
|
|
476
487
|
// one — and swallowing it would lose the change of ticket a person actually made.
|
|
477
488
|
const before = majorOwnersToward(index, day, -1);
|
|
478
489
|
const after = majorOwnersToward(index, day, 1);
|
|
479
|
-
return before === undefined || after === undefined ? run : { ...run, bucketIds: before };
|
|
490
|
+
return before === undefined || after === undefined ? run : { ...run, bucketIds: before, weights: undefined };
|
|
480
491
|
});
|
|
481
492
|
// Phase two: between tickets that were genuinely worked on, ownership holds for the floor.
|
|
482
493
|
let tenant = null;
|
|
@@ -499,7 +510,7 @@ export const applyDwellFloor = (slices, options) => {
|
|
|
499
510
|
}
|
|
500
511
|
const overlapsTenant = tenant !== null && run.bucketIds.some((id) => tenant.bucketIds.includes(id));
|
|
501
512
|
if (tenant !== null && sameDay && run.startMs - tenant.sinceMs < dwellMs && !overlapsTenant) {
|
|
502
|
-
held.push({ ...run, bucketIds: tenant.bucketIds });
|
|
513
|
+
held.push({ ...run, bucketIds: tenant.bucketIds, weights: undefined });
|
|
503
514
|
continue;
|
|
504
515
|
}
|
|
505
516
|
tenant = { bucketIds: run.bucketIds, sinceMs: run.startMs };
|
|
@@ -585,8 +596,10 @@ const wholeSeconds = (spans, totalSeconds) => {
|
|
|
585
596
|
* Issue Key that time belongs to.
|
|
586
597
|
*
|
|
587
598
|
* A slice's duration is divided, never duplicated, so the total across every bucket can never
|
|
588
|
-
* exceed the wall clock of the day. With a positive dwell floor,
|
|
589
|
-
*
|
|
599
|
+
* exceed the wall clock of the day. With a positive dwell floor, each ticket receives one packed
|
|
600
|
+
* block per connected cluster, ordered by its first activity. Touching same-ticket blocks coalesce
|
|
601
|
+
* after packing; idle gaps remain separate. Sub-minute shares fold into another ranked ticket when
|
|
602
|
+
* the cluster has a writable minute; zero preserves raw shares.
|
|
590
603
|
*/
|
|
591
604
|
const shareBetweenBuckets = (spansByBucket, options) => {
|
|
592
605
|
const totals = new Map();
|
|
@@ -603,7 +616,7 @@ const shareBetweenBuckets = (spansByBucket, options) => {
|
|
|
603
616
|
...run,
|
|
604
617
|
bucketIds: run.bucketIds.filter((id) => !options.coveredByCertain(id, run.bucketIds))
|
|
605
618
|
})), options);
|
|
606
|
-
const runs = scheduleRuns(originalRuns, options.dwellSeconds, options.attributed);
|
|
619
|
+
const runs = scheduleRuns(originalRuns, options.dwellSeconds, options.attributed, options.priority);
|
|
607
620
|
for (const run of originalRuns) {
|
|
608
621
|
for (const id of run.bucketIds)
|
|
609
622
|
add(id, localDay(new Date(run.startMs)), 0, run.endMs - run.startMs);
|
|
@@ -617,8 +630,10 @@ const shareBetweenBuckets = (spansByBucket, options) => {
|
|
|
617
630
|
const duration = run.endMs - run.startMs;
|
|
618
631
|
// Divided, never duplicated: this is the one place an instant becomes seconds, and it is the
|
|
619
632
|
// same number that reaches the row's total and the block a person accepts.
|
|
620
|
-
const
|
|
621
|
-
|
|
633
|
+
const totalWeight = run.bucketIds.reduce((sum, id) => sum + (run.weights?.get(id) ?? 1), 0);
|
|
634
|
+
let cursorMs = run.startMs;
|
|
635
|
+
for (const bucketId of run.bucketIds) {
|
|
636
|
+
const creditedMs = duration * (run.weights?.get(bucketId) ?? 1) / totalWeight;
|
|
622
637
|
add(bucketId, day, creditedMs, 0);
|
|
623
638
|
const byDay = settlementEnds.get(bucketId) ?? new Map();
|
|
624
639
|
byDay.set(day, Math.max(byDay.get(day) ?? 0, run.settlementEndMs));
|
|
@@ -628,10 +643,11 @@ const shareBetweenBuckets = (spansByBucket, options) => {
|
|
|
628
643
|
sourceStarts.set(bucketId, startsByDay);
|
|
629
644
|
runSpans.set(bucketId, [...(runSpans.get(bucketId) ?? []), {
|
|
630
645
|
creditedMs,
|
|
631
|
-
endMs: options.dwellSeconds > 0 ?
|
|
632
|
-
startMs: options.dwellSeconds > 0 ?
|
|
646
|
+
endMs: options.dwellSeconds > 0 ? cursorMs + creditedMs : run.endMs,
|
|
647
|
+
startMs: options.dwellSeconds > 0 ? cursorMs : run.startMs,
|
|
633
648
|
sourceStartMs: run.sourceStartMs
|
|
634
649
|
}]);
|
|
650
|
+
cursorMs += creditedMs;
|
|
635
651
|
}
|
|
636
652
|
}
|
|
637
653
|
const result = new Map();
|
|
@@ -671,6 +687,8 @@ const weakerConfidence = (a, b) => {
|
|
|
671
687
|
const bucketId = (kind, ticketKey) => `${kind}\u0000${ticketKey ?? ""}`;
|
|
672
688
|
class MissingDayAttributionError extends Data.TaggedError("MissingDayAttributionError") {
|
|
673
689
|
}
|
|
690
|
+
class InvalidAttributionWeightError extends Data.TaggedError("InvalidAttributionWeightError") {
|
|
691
|
+
}
|
|
674
692
|
/**
|
|
675
693
|
* Fold per-session active windows onto Issue Keys, dividing every overlap between *distinct* keys.
|
|
676
694
|
*
|
|
@@ -693,34 +711,50 @@ export const splitCredits = (windows, attributions, options) => {
|
|
|
693
711
|
const dayKey = (bucketId, day) => `${bucketId}\u0000${day}`;
|
|
694
712
|
for (const session of windows) {
|
|
695
713
|
const attribution = bySession.get(session.sessionId);
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
+
const targets = attribution?.targets ?? [{ ticketKey: attribution?.ticketKey ?? null, weight: 1 }];
|
|
715
|
+
if (targets.some((target) => !Number.isFinite(target.weight) || target.weight <= 0)) {
|
|
716
|
+
throw new InvalidAttributionWeightError({ message: "Split targets require finite positive weights" });
|
|
717
|
+
}
|
|
718
|
+
const allowed = targets.filter((target) => target.ticketKey === null || options?.ignoredTickets?.has(target.ticketKey) !== true);
|
|
719
|
+
const totalWeight = allowed.reduce((sum, target) => sum + target.weight, 0);
|
|
720
|
+
for (const target of allowed) {
|
|
721
|
+
const ticketKey = target.ticketKey;
|
|
722
|
+
if (ticketKey !== null && options?.ignoredTickets?.has(ticketKey) === true)
|
|
723
|
+
continue;
|
|
724
|
+
const kind = ticketKey === null
|
|
725
|
+
? "unattributed"
|
|
726
|
+
: attribution?.belowConfidenceFloor === true
|
|
727
|
+
? "withheld"
|
|
728
|
+
: "attributed";
|
|
729
|
+
const id = bucketId(kind, ticketKey);
|
|
730
|
+
spansByBucket.set(id, [
|
|
731
|
+
...(spansByBucket.get(id) ?? []),
|
|
732
|
+
...session.spans.map((span) => ({
|
|
733
|
+
...span,
|
|
734
|
+
weight: target.weight / totalWeight
|
|
735
|
+
}))
|
|
736
|
+
]);
|
|
737
|
+
// Session windows are already day-bounded, so a span belongs to exactly one day.
|
|
738
|
+
for (const span of session.spans) {
|
|
739
|
+
const key = dayKey(id, localDay(new Date(span.startMs)));
|
|
740
|
+
sessionsByBucketDay.set(key, (sessionsByBucketDay.get(key) ?? new Set()).add(session.sessionId));
|
|
741
|
+
const previous = evidenceByBucketDay.get(key);
|
|
742
|
+
const signal = attribution?.signal ?? "none";
|
|
743
|
+
const confidence = attribution?.confidence ?? null;
|
|
744
|
+
evidenceByBucketDay.set(key, {
|
|
745
|
+
signal: previous === undefined || signalRank(signal) > signalRank(previous.signal) ? signal : previous.signal,
|
|
746
|
+
confidence: previous === undefined ? confidence : weakerConfidence(previous.confidence, confidence)
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
const existing = metaByBucket.get(id);
|
|
750
|
+
metaByBucket.set(id, {
|
|
751
|
+
kind,
|
|
752
|
+
ticketKey,
|
|
753
|
+
sessions: new Set([...(existing?.sessions ?? []), session.sessionId])
|
|
714
754
|
});
|
|
715
755
|
}
|
|
716
|
-
const existing = metaByBucket.get(id);
|
|
717
|
-
metaByBucket.set(id, {
|
|
718
|
-
kind,
|
|
719
|
-
ticketKey,
|
|
720
|
-
sessions: new Set([...(existing?.sessions ?? []), session.sessionId])
|
|
721
|
-
});
|
|
722
756
|
}
|
|
723
|
-
const shared = shareBetweenBuckets(
|
|
757
|
+
const shared = shareBetweenBuckets(spansByBucket, {
|
|
724
758
|
attributed: (id) => metaByBucket.get(id)?.kind === "attributed",
|
|
725
759
|
coveredByCertain: (id, activeIds) => {
|
|
726
760
|
const held = metaByBucket.get(id);
|
|
@@ -729,7 +763,11 @@ export const splitCredits = (windows, attributions, options) => {
|
|
|
729
763
|
return certain?.kind === "attributed" && certain.ticketKey === held.ticketKey;
|
|
730
764
|
});
|
|
731
765
|
},
|
|
732
|
-
dwellSeconds: options?.dwellSeconds ?? DEFAULT_DWELL_SECONDS
|
|
766
|
+
dwellSeconds: options?.dwellSeconds ?? DEFAULT_DWELL_SECONDS,
|
|
767
|
+
priority: (id, day) => {
|
|
768
|
+
const ticketKey = metaByBucket.get(id)?.ticketKey;
|
|
769
|
+
return ticketKey === null || ticketKey === undefined ? 0 : options?.priority?.(ticketKey, day) ?? 0;
|
|
770
|
+
}
|
|
733
771
|
});
|
|
734
772
|
const attributed = [];
|
|
735
773
|
const withheld = [];
|