crewly 1.20.287 → 1.20.289
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/dist/backend/backend/src/constants.d.ts +55 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +56 -1
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts +8 -0
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js +18 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +30 -2
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/resource-mode-protection.d.ts +39 -0
- package/dist/backend/backend/src/services/agent/resource-mode-protection.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/resource-mode-protection.js +54 -0
- package/dist/backend/backend/src/services/agent/resource-mode-protection.js.map +1 -0
- package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts +32 -3
- 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 +95 -9
- package/dist/backend/backend/src/services/agent/resource-mode.service.js.map +1 -1
- package/dist/backend/backend/src/services/briefing/briefing.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/briefing/briefing.service.js +3 -0
- package/dist/backend/backend/src/services/briefing/briefing.service.js.map +1 -1
- package/dist/backend/backend/src/services/drive/drive-briefing-snapshot.d.ts +17 -0
- package/dist/backend/backend/src/services/drive/drive-briefing-snapshot.d.ts.map +1 -1
- package/dist/backend/backend/src/services/drive/drive-briefing-snapshot.js +64 -3
- package/dist/backend/backend/src/services/drive/drive-briefing-snapshot.js.map +1 -1
- package/dist/backend/backend/src/services/drive/drive-briefing.contract.d.ts +15 -0
- package/dist/backend/backend/src/services/drive/drive-briefing.contract.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts +17 -1
- package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.js +1 -1
- package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/project-ticket.service.d.ts +5 -0
- package/dist/backend/backend/src/services/project-tickets/project-ticket.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/project-ticket.service.js +1 -1
- package/dist/backend/backend/src/services/project-tickets/project-ticket.service.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.d.ts +160 -0
- package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.d.ts.map +1 -0
- package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.js +256 -0
- package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.js.map +1 -0
- package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.service.d.ts +135 -0
- package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.service.js +366 -0
- package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.service.js.map +1 -0
- package/dist/backend/backend/src/services/v3/ticket-hygiene.d.ts +22 -0
- package/dist/backend/backend/src/services/v3/ticket-hygiene.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/ticket-hygiene.js +34 -1
- package/dist/backend/backend/src/services/v3/ticket-hygiene.js.map +1 -1
- package/dist/backend/backend/src/services/v3/ticket-intake.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/ticket-intake.service.js +1 -3
- package/dist/backend/backend/src/services/v3/ticket-intake.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/ticket-review.service.d.ts +2 -0
- package/dist/backend/backend/src/services/v3/ticket-review.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/ticket-review.service.js +35 -7
- package/dist/backend/backend/src/services/v3/ticket-review.service.js.map +1 -1
- package/dist/backend/backend/src/types/v2/work-item.types.d.ts +1 -1
- package/dist/backend/backend/src/types/v2/work-item.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/work-item.types.js +1 -0
- package/dist/backend/backend/src/types/v2/work-item.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +55 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +56 -1
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts +32 -3
- 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 +95 -9
- package/dist/cli/backend/src/services/agent/resource-mode.service.js.map +1 -1
- package/dist/cli/backend/src/services/project-tickets/project-ticket.service.d.ts +5 -0
- package/dist/cli/backend/src/services/project-tickets/project-ticket.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/project-tickets/project-ticket.service.js +1 -1
- package/dist/cli/backend/src/services/project-tickets/project-ticket.service.js.map +1 -1
- package/dist/cli/backend/src/services/v3/ticket-hygiene.d.ts +22 -0
- package/dist/cli/backend/src/services/v3/ticket-hygiene.d.ts.map +1 -1
- package/dist/cli/backend/src/services/v3/ticket-hygiene.js +34 -1
- package/dist/cli/backend/src/services/v3/ticket-hygiene.js.map +1 -1
- package/dist/cli/backend/src/services/v3/ticket-intake.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/v3/ticket-intake.service.js +1 -3
- package/dist/cli/backend/src/services/v3/ticket-intake.service.js.map +1 -1
- package/dist/cli/backend/src/services/v3/ticket-review.service.d.ts +2 -0
- package/dist/cli/backend/src/services/v3/ticket-review.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/v3/ticket-review.service.js +35 -7
- package/dist/cli/backend/src/services/v3/ticket-review.service.js.map +1 -1
- package/dist/cli/backend/src/types/v2/work-item.types.d.ts +1 -1
- package/dist/cli/backend/src/types/v2/work-item.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/v2/work-item.types.js +1 -0
- package/dist/cli/backend/src/types/v2/work-item.types.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket hygiene service — keeps project tickets honest.
|
|
3
|
+
*
|
|
4
|
+
* Two jobs, both cheap:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Auto-reconcile (no agent).** Each sweep closes or advances tickets
|
|
7
|
+
* whose state is objectively settled (the linked WorkItem is verified /
|
|
8
|
+
* done) and flags tickets whose assignee or team no longer exists. Every
|
|
9
|
+
* change is logged and written to the ticket's Log with the reason
|
|
10
|
+
* `auto-reconcile`.
|
|
11
|
+
* 2. **Stale review (one lead, once a day).** Open tickets with no activity
|
|
12
|
+
* past a threshold are batched into ONE WorkItem per team for the team lead
|
|
13
|
+
* (the orchestrator when the team has none) to close, cancel or annotate.
|
|
14
|
+
*
|
|
15
|
+
* Loop safety: at most one live review item per team, at most one batch per
|
|
16
|
+
* team per day, a ticket is not re-sent within the cooldown, and the sweep's
|
|
17
|
+
* own Log lines never count as ticket activity. Nothing here messages the owner.
|
|
18
|
+
*
|
|
19
|
+
* @module services/project-tickets/ticket-hygiene.service
|
|
20
|
+
*/
|
|
21
|
+
import { type ComponentLogger } from '../core/logger.service.js';
|
|
22
|
+
import { type WorkItem } from '../../types/v2/work-item.types.js';
|
|
23
|
+
import type { ProjectTicketService } from './project-ticket.service.js';
|
|
24
|
+
import { type ChainEnd, type ProjectTicketDirectory } from './project-ticket-workflow.service.js';
|
|
25
|
+
/** The pool operations the service uses. */
|
|
26
|
+
export interface TicketHygienePool {
|
|
27
|
+
getAllItems(): Promise<WorkItem[]>;
|
|
28
|
+
addToPool(workItem: WorkItem): Promise<void>;
|
|
29
|
+
cancelQueued(workItemId: string, reason: string): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
/** Dependencies (injectable for tests). */
|
|
32
|
+
export interface TicketHygieneDeps {
|
|
33
|
+
tickets: Pick<ProjectTicketService, 'list' | 'mutate'>;
|
|
34
|
+
pool: TicketHygienePool;
|
|
35
|
+
directory: ProjectTicketDirectory;
|
|
36
|
+
/** Follow a WorkItem to where its work stands (the workflow service's) */
|
|
37
|
+
followChain: (workItemId: string) => Promise<ChainEnd>;
|
|
38
|
+
/** Where the bookkeeping lives */
|
|
39
|
+
stateFile: string;
|
|
40
|
+
logger?: ComponentLogger;
|
|
41
|
+
now?: () => Date;
|
|
42
|
+
}
|
|
43
|
+
/** One automatic change, as logged. */
|
|
44
|
+
export interface HygieneChange {
|
|
45
|
+
projectId: string;
|
|
46
|
+
ticketId: string;
|
|
47
|
+
kind: 'closed' | 'advanced' | 'flagged' | 'unflagged';
|
|
48
|
+
message: string;
|
|
49
|
+
}
|
|
50
|
+
/** What a run did. */
|
|
51
|
+
export interface HygieneRunResult {
|
|
52
|
+
changes: HygieneChange[];
|
|
53
|
+
/** Review items created (one per team) */
|
|
54
|
+
reviewItems: Array<{
|
|
55
|
+
teamKey: string;
|
|
56
|
+
target: string;
|
|
57
|
+
workItemId: string;
|
|
58
|
+
tickets: number;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
export declare class TicketHygieneService {
|
|
62
|
+
private readonly deps;
|
|
63
|
+
private static instance;
|
|
64
|
+
private readonly logger;
|
|
65
|
+
private state;
|
|
66
|
+
private timer;
|
|
67
|
+
private firstTimer;
|
|
68
|
+
private running;
|
|
69
|
+
/**
|
|
70
|
+
* @param deps - Ticket store, pool, directory, chain follower, state file
|
|
71
|
+
*/
|
|
72
|
+
constructor(deps: TicketHygieneDeps);
|
|
73
|
+
/** @returns The wired instance, or null */
|
|
74
|
+
static getInstance(): TicketHygieneService | null;
|
|
75
|
+
/** @param service - Instance to install (null clears) */
|
|
76
|
+
static setInstance(service: TicketHygieneService | null): void;
|
|
77
|
+
/**
|
|
78
|
+
* Start the periodic sweep: a first run a few minutes after boot, then every
|
|
79
|
+
* {@link TICKET_HYGIENE_CONSTANTS.SWEEP_INTERVAL_MS}.
|
|
80
|
+
*
|
|
81
|
+
* @param intervalMs - Sweep interval (0 disables the timers)
|
|
82
|
+
* @param firstDelayMs - Delay of the first run
|
|
83
|
+
*/
|
|
84
|
+
start(intervalMs?: number, firstDelayMs?: number): void;
|
|
85
|
+
/** Stop the timers. */
|
|
86
|
+
stop(): void;
|
|
87
|
+
/**
|
|
88
|
+
* One sweep: reconcile settled / orphaned tickets, then queue today's lead
|
|
89
|
+
* reviews. Safe to call at any time (a run already in progress is skipped).
|
|
90
|
+
*
|
|
91
|
+
* @returns What changed and which review items went out
|
|
92
|
+
*/
|
|
93
|
+
runOnce(): Promise<HygieneRunResult>;
|
|
94
|
+
/**
|
|
95
|
+
* Close or advance an `in_progress` / `review` ticket whose linked work is
|
|
96
|
+
* all verified / done, honouring the done gate: a ticket with `ownerReview`
|
|
97
|
+
* lands in `review` (and stays there, waiting for the owner) instead of `done`.
|
|
98
|
+
*
|
|
99
|
+
* Only these two statuses are touched, so a person's move of a ticket back to
|
|
100
|
+
* `ready` / `backlog` is never fought.
|
|
101
|
+
*
|
|
102
|
+
* @param projectId - Project id (for the log)
|
|
103
|
+
* @param ticket - The ticket
|
|
104
|
+
* @param poolItems - Pool snapshot
|
|
105
|
+
* @returns The change, or null when nothing is settled
|
|
106
|
+
*/
|
|
107
|
+
private reconcileSettled;
|
|
108
|
+
/**
|
|
109
|
+
* Keep the `orphaned` label in step with whether the assignee / team still
|
|
110
|
+
* exist. Bookkeeping only: `updatedAt` is left alone so a flag never makes a
|
|
111
|
+
* stale ticket look fresh.
|
|
112
|
+
*
|
|
113
|
+
* @param projectId - Project id (for the log)
|
|
114
|
+
* @param ticket - The ticket
|
|
115
|
+
* @param orphan - Missing parts, or null
|
|
116
|
+
* @returns The change, or null when the label already matches
|
|
117
|
+
*/
|
|
118
|
+
private reconcileOrphanFlag;
|
|
119
|
+
/**
|
|
120
|
+
* Send today's batches: one WorkItem per team that has stale tickets, no
|
|
121
|
+
* more than one per team per day, none while a previous one is still live.
|
|
122
|
+
*
|
|
123
|
+
* @param tickets - Open tickets after reconcile
|
|
124
|
+
* @param teams - All teams
|
|
125
|
+
* @param orphans - Orphaned tickets
|
|
126
|
+
* @param nowMs - Clock
|
|
127
|
+
* @returns The items created
|
|
128
|
+
*/
|
|
129
|
+
private queueReviews;
|
|
130
|
+
private now;
|
|
131
|
+
private pruneSent;
|
|
132
|
+
private loadState;
|
|
133
|
+
private saveState;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=ticket-hygiene.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticket-hygiene.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/project-tickets/ticket-hygiene.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAEhF,OAAO,EAA4C,KAAK,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAI5G,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAiB,KAAK,QAAQ,EAAE,KAAK,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAcjH,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,WAAW,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IACnC,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE;AAED,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,IAAI,CAAC,oBAAoB,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC;IACvD,IAAI,EAAE,iBAAiB,CAAC;IACxB,SAAS,EAAE,sBAAsB,CAAC;IAClC,0EAA0E;IAC1E,WAAW,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvD,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAUD,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,WAAW,CAAC;IACtD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,sBAAsB;AACtB,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,0CAA0C;IAC1C,WAAW,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9F;AAED,qBAAa,oBAAoB;IAYnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAXjC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAqC;IAE5D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,UAAU,CAA8C;IAChE,OAAO,CAAC,OAAO,CAAS;IAExB;;OAEG;gBAC0B,IAAI,EAAE,iBAAiB;IAIpD,2CAA2C;IAC3C,MAAM,CAAC,WAAW,IAAI,oBAAoB,GAAG,IAAI;IAIjD,yDAAyD;IACzD,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,GAAG,IAAI;IAI9D;;;;;;OAMG;IACH,KAAK,CAAC,UAAU,GAAE,MAA4B,EAAE,YAAY,GAAE,MAA+B,GAAG,IAAI;IAcpG,uBAAuB;IACvB,IAAI,IAAI,IAAI;IAOZ;;;;;OAKG;IACG,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC;IA4D1C;;;;;;;;;;;;OAYG;YACW,gBAAgB;IAiC9B;;;;;;;;;OASG;YACW,mBAAmB;IAwBjC;;;;;;;;;OASG;YACW,YAAY;IA6E1B,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,SAAS;YAKH,SAAS;YAUT,SAAS;CAQxB"}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket hygiene service — keeps project tickets honest.
|
|
3
|
+
*
|
|
4
|
+
* Two jobs, both cheap:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Auto-reconcile (no agent).** Each sweep closes or advances tickets
|
|
7
|
+
* whose state is objectively settled (the linked WorkItem is verified /
|
|
8
|
+
* done) and flags tickets whose assignee or team no longer exists. Every
|
|
9
|
+
* change is logged and written to the ticket's Log with the reason
|
|
10
|
+
* `auto-reconcile`.
|
|
11
|
+
* 2. **Stale review (one lead, once a day).** Open tickets with no activity
|
|
12
|
+
* past a threshold are batched into ONE WorkItem per team for the team lead
|
|
13
|
+
* (the orchestrator when the team has none) to close, cancel or annotate.
|
|
14
|
+
*
|
|
15
|
+
* Loop safety: at most one live review item per team, at most one batch per
|
|
16
|
+
* team per day, a ticket is not re-sent within the cooldown, and the sweep's
|
|
17
|
+
* own Log lines never count as ticket activity. Nothing here messages the owner.
|
|
18
|
+
*
|
|
19
|
+
* @module services/project-tickets/ticket-hygiene.service
|
|
20
|
+
*/
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
import { ORCHESTRATOR_SESSION_NAME, TICKET_HYGIENE_CONSTANTS } from '../../constants.js';
|
|
23
|
+
import { LoggerService } from '../core/logger.service.js';
|
|
24
|
+
import { createWorkItem, MAX_BRIEF_MARKDOWN_BYTES } from '../../types/v2/work-item.types.js';
|
|
25
|
+
import { readProjectTicketLink } from '../../types/project-ticket.types.js';
|
|
26
|
+
import { atomicWriteJson, safeReadJson } from '../../utils/file-io.utils.js';
|
|
27
|
+
import { isTeamPausedNow } from '../team/team-pause.registry.js';
|
|
28
|
+
import { LIVE_STATUSES } from './project-ticket-workflow.service.js';
|
|
29
|
+
import { OPEN_TICKET_STATUSES, buildReviewBrief, orphanReason, selectReviewBatches, ticketKey, } from './ticket-hygiene.js';
|
|
30
|
+
const C = TICKET_HYGIENE_CONSTANTS;
|
|
31
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
32
|
+
export class TicketHygieneService {
|
|
33
|
+
deps;
|
|
34
|
+
static instance = null;
|
|
35
|
+
logger;
|
|
36
|
+
state = null;
|
|
37
|
+
timer = null;
|
|
38
|
+
firstTimer = null;
|
|
39
|
+
running = false;
|
|
40
|
+
/**
|
|
41
|
+
* @param deps - Ticket store, pool, directory, chain follower, state file
|
|
42
|
+
*/
|
|
43
|
+
constructor(deps) {
|
|
44
|
+
this.deps = deps;
|
|
45
|
+
this.logger = deps.logger ?? LoggerService.getInstance().createComponentLogger('TicketHygiene');
|
|
46
|
+
}
|
|
47
|
+
/** @returns The wired instance, or null */
|
|
48
|
+
static getInstance() {
|
|
49
|
+
return TicketHygieneService.instance;
|
|
50
|
+
}
|
|
51
|
+
/** @param service - Instance to install (null clears) */
|
|
52
|
+
static setInstance(service) {
|
|
53
|
+
TicketHygieneService.instance = service;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Start the periodic sweep: a first run a few minutes after boot, then every
|
|
57
|
+
* {@link TICKET_HYGIENE_CONSTANTS.SWEEP_INTERVAL_MS}.
|
|
58
|
+
*
|
|
59
|
+
* @param intervalMs - Sweep interval (0 disables the timers)
|
|
60
|
+
* @param firstDelayMs - Delay of the first run
|
|
61
|
+
*/
|
|
62
|
+
start(intervalMs = C.SWEEP_INTERVAL_MS, firstDelayMs = C.FIRST_SWEEP_DELAY_MS) {
|
|
63
|
+
this.stop();
|
|
64
|
+
if (intervalMs <= 0)
|
|
65
|
+
return;
|
|
66
|
+
const run = () => {
|
|
67
|
+
void this.runOnce().catch((err) => this.logger.warn('Ticket hygiene sweep failed (non-fatal)', { error: err instanceof Error ? err.message : String(err) }));
|
|
68
|
+
};
|
|
69
|
+
this.firstTimer = setTimeout(run, firstDelayMs);
|
|
70
|
+
this.firstTimer.unref?.();
|
|
71
|
+
this.timer = setInterval(run, intervalMs);
|
|
72
|
+
this.timer.unref?.();
|
|
73
|
+
}
|
|
74
|
+
/** Stop the timers. */
|
|
75
|
+
stop() {
|
|
76
|
+
if (this.timer)
|
|
77
|
+
clearInterval(this.timer);
|
|
78
|
+
if (this.firstTimer)
|
|
79
|
+
clearTimeout(this.firstTimer);
|
|
80
|
+
this.timer = null;
|
|
81
|
+
this.firstTimer = null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* One sweep: reconcile settled / orphaned tickets, then queue today's lead
|
|
85
|
+
* reviews. Safe to call at any time (a run already in progress is skipped).
|
|
86
|
+
*
|
|
87
|
+
* @returns What changed and which review items went out
|
|
88
|
+
*/
|
|
89
|
+
async runOnce() {
|
|
90
|
+
const result = { changes: [], reviewItems: [] };
|
|
91
|
+
if (this.running)
|
|
92
|
+
return result;
|
|
93
|
+
this.running = true;
|
|
94
|
+
try {
|
|
95
|
+
const [projects, teams] = await Promise.all([this.deps.directory.getProjects(), this.deps.directory.getTeams()]);
|
|
96
|
+
const poolItems = await this.deps.pool.getAllItems().catch(() => []);
|
|
97
|
+
const nowMs = this.now().getTime();
|
|
98
|
+
const all = [];
|
|
99
|
+
const orphans = new Map();
|
|
100
|
+
for (const project of projects) {
|
|
101
|
+
let tickets;
|
|
102
|
+
try {
|
|
103
|
+
tickets = (await this.deps.tickets.list(project.path)).tickets;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
for (const listed of tickets) {
|
|
109
|
+
if (!OPEN_TICKET_STATUSES.has(listed.status))
|
|
110
|
+
continue;
|
|
111
|
+
let t = listed;
|
|
112
|
+
try {
|
|
113
|
+
const settled = await this.reconcileSettled(project.id, t, poolItems);
|
|
114
|
+
if (settled) {
|
|
115
|
+
result.changes.push(settled.change);
|
|
116
|
+
t = settled.ticket;
|
|
117
|
+
}
|
|
118
|
+
if (!OPEN_TICKET_STATUSES.has(t.status))
|
|
119
|
+
continue;
|
|
120
|
+
const orphan = orphanReason(t, teams);
|
|
121
|
+
if (orphan)
|
|
122
|
+
orphans.set(ticketKey(project.path, t.id), orphan);
|
|
123
|
+
const flag = await this.reconcileOrphanFlag(project.id, t, orphan);
|
|
124
|
+
if (flag) {
|
|
125
|
+
result.changes.push(flag.change);
|
|
126
|
+
t = flag.ticket;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
this.logger.warn('Ticket hygiene could not reconcile a ticket', {
|
|
131
|
+
projectId: project.id,
|
|
132
|
+
ticketId: t.id,
|
|
133
|
+
error: err instanceof Error ? err.message : String(err),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (OPEN_TICKET_STATUSES.has(t.status))
|
|
137
|
+
all.push({ ...t, projectId: project.id, projectName: project.name, projectPath: project.path });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
result.reviewItems = await this.queueReviews(all, teams, orphans, nowMs);
|
|
141
|
+
if (result.changes.length > 0 || result.reviewItems.length > 0) {
|
|
142
|
+
this.logger.info('Ticket hygiene sweep done', { changes: result.changes.length, reviewItems: result.reviewItems.length });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
this.running = false;
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Auto-reconcile
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
/**
|
|
154
|
+
* Close or advance an `in_progress` / `review` ticket whose linked work is
|
|
155
|
+
* all verified / done, honouring the done gate: a ticket with `ownerReview`
|
|
156
|
+
* lands in `review` (and stays there, waiting for the owner) instead of `done`.
|
|
157
|
+
*
|
|
158
|
+
* Only these two statuses are touched, so a person's move of a ticket back to
|
|
159
|
+
* `ready` / `backlog` is never fought.
|
|
160
|
+
*
|
|
161
|
+
* @param projectId - Project id (for the log)
|
|
162
|
+
* @param ticket - The ticket
|
|
163
|
+
* @param poolItems - Pool snapshot
|
|
164
|
+
* @returns The change, or null when nothing is settled
|
|
165
|
+
*/
|
|
166
|
+
async reconcileSettled(projectId, ticket, poolItems) {
|
|
167
|
+
if (ticket.status !== 'in_progress' && ticket.status !== 'review')
|
|
168
|
+
return null;
|
|
169
|
+
if (ticket.status === 'review' && ticket.ownerReview)
|
|
170
|
+
return null;
|
|
171
|
+
const root = path.resolve(ticket.projectPath);
|
|
172
|
+
const linked = poolItems.filter((wi) => {
|
|
173
|
+
const link = readProjectTicketLink(wi.metadata);
|
|
174
|
+
return !!link && link.id === ticket.id && path.resolve(link.projectPath) === root;
|
|
175
|
+
});
|
|
176
|
+
if (linked.some((wi) => LIVE_STATUSES.has(wi.status)))
|
|
177
|
+
return null;
|
|
178
|
+
let doneWi = null;
|
|
179
|
+
if (ticket.workItemId) {
|
|
180
|
+
const end = await this.deps.followChain(ticket.workItemId);
|
|
181
|
+
if (end.kind === 'success')
|
|
182
|
+
doneWi = end.wi;
|
|
183
|
+
}
|
|
184
|
+
else if (linked.length > 0 && linked.every((wi) => wi.status === 'verified' || wi.status === 'done')) {
|
|
185
|
+
doneWi = linked[linked.length - 1];
|
|
186
|
+
}
|
|
187
|
+
if (!doneWi)
|
|
188
|
+
return null;
|
|
189
|
+
const to = ticket.ownerReview ? 'review' : 'done';
|
|
190
|
+
if (to === ticket.status)
|
|
191
|
+
return null;
|
|
192
|
+
let message = '';
|
|
193
|
+
const updated = await this.deps.tickets.mutate(ticket.projectPath, ticket.id, C.ACTOR, (t) => {
|
|
194
|
+
// Re-checked under the lock: a person or the event sync may have moved it.
|
|
195
|
+
if (t.status !== ticket.status)
|
|
196
|
+
return null;
|
|
197
|
+
message = `${t.status} → ${to} — ${C.REASON}: WorkItem ${doneWi.id} is ${doneWi.status}${to === 'review' ? ' (waiting for the owner)' : ''}`;
|
|
198
|
+
return { fields: { status: to, workItemId: doneWi.id }, log: [message] };
|
|
199
|
+
});
|
|
200
|
+
if (!message)
|
|
201
|
+
return null;
|
|
202
|
+
this.logger.info(`Ticket ${ticket.id} moved ${ticket.status} → ${to} (${C.REASON}: WorkItem ${doneWi.id} is ${doneWi.status})`, { projectId, ticketId: ticket.id });
|
|
203
|
+
return { change: { projectId, ticketId: ticket.id, kind: to === 'done' ? 'closed' : 'advanced', message }, ticket: updated };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Keep the `orphaned` label in step with whether the assignee / team still
|
|
207
|
+
* exist. Bookkeeping only: `updatedAt` is left alone so a flag never makes a
|
|
208
|
+
* stale ticket look fresh.
|
|
209
|
+
*
|
|
210
|
+
* @param projectId - Project id (for the log)
|
|
211
|
+
* @param ticket - The ticket
|
|
212
|
+
* @param orphan - Missing parts, or null
|
|
213
|
+
* @returns The change, or null when the label already matches
|
|
214
|
+
*/
|
|
215
|
+
async reconcileOrphanFlag(projectId, ticket, orphan) {
|
|
216
|
+
const has = ticket.labels.includes(C.ORPHAN_LABEL);
|
|
217
|
+
if (!!orphan === has)
|
|
218
|
+
return null;
|
|
219
|
+
let message = '';
|
|
220
|
+
const updated = await this.deps.tickets.mutate(ticket.projectPath, ticket.id, C.ACTOR, (t) => {
|
|
221
|
+
const nowHas = t.labels.includes(C.ORPHAN_LABEL);
|
|
222
|
+
if (nowHas === !!orphan)
|
|
223
|
+
return null;
|
|
224
|
+
if (orphan) {
|
|
225
|
+
const what = [orphan.assignee ? `assignee ${orphan.assignee}` : '', orphan.team ? `team ${orphan.team}` : ''].filter(Boolean).join(' and ');
|
|
226
|
+
message = `flagged ${C.ORPHAN_LABEL} — ${C.REASON}: ${what} no longer exists`;
|
|
227
|
+
return { fields: { labels: [...t.labels, C.ORPHAN_LABEL] }, log: [message], keepUpdatedAt: true };
|
|
228
|
+
}
|
|
229
|
+
message = `${C.ORPHAN_LABEL} flag cleared — ${C.REASON}: assignee and team exist again`;
|
|
230
|
+
return { fields: { labels: t.labels.filter((l) => l !== C.ORPHAN_LABEL) }, log: [message], keepUpdatedAt: true };
|
|
231
|
+
});
|
|
232
|
+
if (!message)
|
|
233
|
+
return null;
|
|
234
|
+
this.logger.info(`Ticket ${ticket.id}: ${message}`, { projectId, ticketId: ticket.id });
|
|
235
|
+
return { change: { projectId, ticketId: ticket.id, kind: orphan ? 'flagged' : 'unflagged', message }, ticket: updated };
|
|
236
|
+
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Stale review
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
/**
|
|
241
|
+
* Send today's batches: one WorkItem per team that has stale tickets, no
|
|
242
|
+
* more than one per team per day, none while a previous one is still live.
|
|
243
|
+
*
|
|
244
|
+
* @param tickets - Open tickets after reconcile
|
|
245
|
+
* @param teams - All teams
|
|
246
|
+
* @param orphans - Orphaned tickets
|
|
247
|
+
* @param nowMs - Clock
|
|
248
|
+
* @returns The items created
|
|
249
|
+
*/
|
|
250
|
+
async queueReviews(tickets, teams, orphans, nowMs) {
|
|
251
|
+
const state = await this.loadState();
|
|
252
|
+
this.pruneSent(state, nowMs);
|
|
253
|
+
const skipTeamIds = new Set(teams.filter((t) => isTeamPausedNow(t, nowMs)).map((t) => t.id));
|
|
254
|
+
const batches = selectReviewBatches({ tickets, teams, nowMs, sentAt: state.sent, orphans, skipTeamIds });
|
|
255
|
+
const out = [];
|
|
256
|
+
if (batches.length === 0) {
|
|
257
|
+
await this.saveState();
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
const poolItems = await this.deps.pool.getAllItems().catch(() => []);
|
|
261
|
+
for (const batch of batches) {
|
|
262
|
+
const last = state.teams[batch.key]?.lastSentAt;
|
|
263
|
+
if (last !== undefined && nowMs - last < C.REVIEW_INTERVAL_MS)
|
|
264
|
+
continue;
|
|
265
|
+
// A previous batch still live? Wait for it — unless nobody ever picked it up.
|
|
266
|
+
let live = false;
|
|
267
|
+
for (const wi of poolItems) {
|
|
268
|
+
if (wi.metadata?.kind !== C.REVIEW_METADATA_KIND || wi.metadata?.teamKey !== batch.key || !LIVE_STATUSES.has(wi.status))
|
|
269
|
+
continue;
|
|
270
|
+
const age = nowMs - (Date.parse(wi.createdAt) || nowMs);
|
|
271
|
+
if (wi.status === 'queued' && age >= C.REVIEW_STALE_QUEUED_MS) {
|
|
272
|
+
await this.deps.pool.cancelQueued(wi.id, 'ticket review never picked up; replaced by a fresh one').catch(() => undefined);
|
|
273
|
+
this.logger.info('Stale ticket review cancelled', { teamKey: batch.key, workItemId: wi.id });
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
live = true;
|
|
277
|
+
}
|
|
278
|
+
if (live)
|
|
279
|
+
continue;
|
|
280
|
+
const n = batch.candidates.length;
|
|
281
|
+
const toOrc = batch.target === ORCHESTRATOR_SESSION_NAME;
|
|
282
|
+
const workItem = createWorkItem({
|
|
283
|
+
type: C.REVIEW_WORK_ITEM_TYPE,
|
|
284
|
+
owner: toOrc ? 'orchestrator' : 'team_lead',
|
|
285
|
+
target: batch.target,
|
|
286
|
+
title: `Ticket review: ${batch.teamName} (${n} stale ticket${n === 1 ? '' : 's'})`,
|
|
287
|
+
description: `Close, cancel or update ${n} ticket${n === 1 ? '' : 's'} of ${batch.teamName} that had no activity for a while.`,
|
|
288
|
+
briefMarkdown: capBrief(buildReviewBrief(batch)),
|
|
289
|
+
metadata: {
|
|
290
|
+
kind: C.REVIEW_METADATA_KIND,
|
|
291
|
+
teamKey: batch.key,
|
|
292
|
+
teamId: batch.teamId,
|
|
293
|
+
requiresVerification: false,
|
|
294
|
+
tickets: batch.candidates.map((c) => ({ projectId: c.ticket.projectId, id: c.ticket.id })),
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
workItem.createdAt = new Date(nowMs).toISOString();
|
|
298
|
+
workItem.targetSource = 'assigned';
|
|
299
|
+
try {
|
|
300
|
+
await this.deps.pool.addToPool(workItem);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
this.logger.warn('Could not queue the ticket review', { teamKey: batch.key, error: err instanceof Error ? err.message : String(err) });
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
state.teams[batch.key] = { lastSentAt: nowMs, workItemId: workItem.id };
|
|
307
|
+
for (const c of batch.candidates)
|
|
308
|
+
state.sent[ticketKey(c.ticket.projectPath, c.ticket.id)] = nowMs;
|
|
309
|
+
await this.saveState();
|
|
310
|
+
this.logger.info(`Ticket review for ${batch.teamName} sent to ${batch.target}: ${n} ticket(s)`, {
|
|
311
|
+
teamKey: batch.key,
|
|
312
|
+
workItemId: workItem.id,
|
|
313
|
+
tickets: batch.candidates.map((c) => c.ticket.id),
|
|
314
|
+
});
|
|
315
|
+
out.push({ teamKey: batch.key, target: batch.target, workItemId: workItem.id, tickets: n });
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// State
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
now() {
|
|
323
|
+
return this.deps.now?.() ?? new Date();
|
|
324
|
+
}
|
|
325
|
+
pruneSent(state, nowMs) {
|
|
326
|
+
const cutoff = nowMs - C.STATE_RETENTION_DAYS * DAY_MS;
|
|
327
|
+
for (const [k, at] of Object.entries(state.sent))
|
|
328
|
+
if (at < cutoff)
|
|
329
|
+
delete state.sent[k];
|
|
330
|
+
}
|
|
331
|
+
async loadState() {
|
|
332
|
+
if (this.state)
|
|
333
|
+
return this.state;
|
|
334
|
+
const raw = await safeReadJson(this.deps.stateFile, {});
|
|
335
|
+
this.state = {
|
|
336
|
+
teams: raw.teams && typeof raw.teams === 'object' ? raw.teams : {},
|
|
337
|
+
sent: raw.sent && typeof raw.sent === 'object' ? raw.sent : {},
|
|
338
|
+
};
|
|
339
|
+
return this.state;
|
|
340
|
+
}
|
|
341
|
+
async saveState() {
|
|
342
|
+
if (!this.state)
|
|
343
|
+
return;
|
|
344
|
+
try {
|
|
345
|
+
await atomicWriteJson(this.deps.stateFile, this.state);
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
this.logger.warn('Could not save ticket hygiene state', { error: err instanceof Error ? err.message : String(err) });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Keep a brief within the WorkItem brief budget.
|
|
354
|
+
*
|
|
355
|
+
* @param text - Markdown
|
|
356
|
+
* @returns Text within the budget
|
|
357
|
+
*/
|
|
358
|
+
function capBrief(text) {
|
|
359
|
+
if (Buffer.byteLength(text, 'utf8') <= MAX_BRIEF_MARKDOWN_BYTES)
|
|
360
|
+
return text;
|
|
361
|
+
let out = text.slice(0, MAX_BRIEF_MARKDOWN_BYTES);
|
|
362
|
+
while (Buffer.byteLength(out, 'utf8') > MAX_BRIEF_MARKDOWN_BYTES - 64)
|
|
363
|
+
out = out.slice(0, -64);
|
|
364
|
+
return `${out}\n\n…(truncated — list the tickets with project-tickets)`;
|
|
365
|
+
}
|
|
366
|
+
//# sourceMappingURL=ticket-hygiene.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticket-hygiene.service.js","sourceRoot":"","sources":["../../../../../../backend/src/services/project-tickets/ticket-hygiene.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -41,6 +41,28 @@ import type { Request, UpdateRequestInput } from '../../types/v2/request.types.j
|
|
|
41
41
|
export declare function answerNeedsOwner(ticket: Pick<Request, 'description' | 'intentCategory' | 'kind'> & {
|
|
42
42
|
reply?: Pick<NonNullable<Request['reply']>, 'excerpt'> | undefined;
|
|
43
43
|
}): boolean;
|
|
44
|
+
/** The owner asks where it is / to send it again — never an approval. */
|
|
45
|
+
export declare const RESEND_OR_WHERE: RegExp;
|
|
46
|
+
/**
|
|
47
|
+
* Whether a message the owner left in a 待验收 ticket's thread is an
|
|
48
|
+
* objection: a 打回, or "where is it / send it again".
|
|
49
|
+
*
|
|
50
|
+
* @param text - The owner's message
|
|
51
|
+
* @returns True when the work is not settled by it
|
|
52
|
+
*/
|
|
53
|
+
export declare function isOwnerObjection(text: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Whether the owner already moved on in a 待验收 ticket's thread: they posted
|
|
56
|
+
* after the answer and nothing they said is an objection. Pure
|
|
57
|
+
* acknowledgements are not stored in the discussion (they accept at once), so
|
|
58
|
+
* a later entry is a follow-up the agent answers in the thread, not a
|
|
59
|
+
* complaint about the finished work. Notes written by Crewly itself do not
|
|
60
|
+
* count.
|
|
61
|
+
*
|
|
62
|
+
* @param ticket - The ticket in 待验收
|
|
63
|
+
* @returns True when silence-about-the-work can be taken as acceptance now
|
|
64
|
+
*/
|
|
65
|
+
export declare function ownerMovedOn(ticket: Pick<Request, 'submittedAt' | 'discussion'>): boolean;
|
|
44
66
|
/**
|
|
45
67
|
* The last time anything happened on a ticket: created, updated, answered,
|
|
46
68
|
* submitted or discussed.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ticket-hygiene.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/v3/ticket-hygiene.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAQnF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,aAAa,GAAG,gBAAgB,GAAG,MAAM,CAAC,GAAG;IAAE,KAAK,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG,SAAS,CAAA;CAAE,GACxI,OAAO,CAQT;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW,GAAG,aAAa,GAAG,YAAY,CAAC,GAAG;IAAE,KAAK,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,SAAS,CAAA;CAAE,GAClJ,MAAM,CAKR;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAGnE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,kBAAkB,CAYpF;AAMD,2CAA2C;AAC3C,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,mEAAmE;IACnE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxB;;;;OAIG;IACH,MAAM,EAAE,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAC;CACzC;AAED,sEAAsE;AACtE,MAAM,WAAW,mBAAmB;IAClC,2CAA2C;IAC3C,OAAO,EAAE,OAAO,CAAC;IACjB,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9C,OAAO,EAAE,mBAAmB,EAAE,CAAC;CAChC;AAED,6CAA6C;AAC7C,MAAM,WAAW,oBAAoB;IACnC,wBAAwB;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,EAAE,IAAI,EAAE,oBAAoB,GAAG,mBAAmB,CAuB/G;AAED,4DAA4D;AAC5D,MAAM,WAAW,kBAAkB;IACjC,OAAO,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,kBAAkB,EACzB,IAAI,EAAE,oBAAoB,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAC/C,OAAO,CAAC,mBAAmB,CAAC,CA6C9B"}
|
|
1
|
+
{"version":3,"file":"ticket-hygiene.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/v3/ticket-hygiene.ts"],"names":[],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* @module services/v3/ticket-hygiene
|
|
19
19
|
*/
|
|
20
20
|
import { TICKET_CONSTANTS } from '../../constants.js';
|
|
21
|
-
import { formatTicketNumber, ticketNeedsReview } from '../../types/v2/ticket.types.js';
|
|
21
|
+
import { formatTicketNumber, parseReviewReply, ticketNeedsReview } from '../../types/v2/ticket.types.js';
|
|
22
22
|
import { askText } from './ticket-ask-classifier.js';
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
// Rules
|
|
@@ -55,6 +55,39 @@ export function answerNeedsOwner(ticket) {
|
|
|
55
55
|
// Ends on a question to him, or asks for an OK / a choice anywhere.
|
|
56
56
|
return /[??]$/.test(answer) || R.OWNER_QUESTION.test(answer);
|
|
57
57
|
}
|
|
58
|
+
/** The owner asks where it is / to send it again — never an approval. */
|
|
59
|
+
export const RESEND_OR_WHERE = /(再发|重新发|重发|再给我|发一下|发我一下|在哪|哪里|哪儿|没看到|没收到|看不到|找不到|收不到|链接呢|发到哪|where|resend|send (?:it |the \w+ )?again|didn'?t (?:get|see|receive)|can'?t (?:find|see)|no link)/i;
|
|
60
|
+
/**
|
|
61
|
+
* Whether a message the owner left in a 待验收 ticket's thread is an
|
|
62
|
+
* objection: a 打回, or "where is it / send it again".
|
|
63
|
+
*
|
|
64
|
+
* @param text - The owner's message
|
|
65
|
+
* @returns True when the work is not settled by it
|
|
66
|
+
*/
|
|
67
|
+
export function isOwnerObjection(text) {
|
|
68
|
+
const t = (text ?? '').trim();
|
|
69
|
+
return parseReviewReply(t)?.action === 'reject' || RESEND_OR_WHERE.test(t);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether the owner already moved on in a 待验收 ticket's thread: they posted
|
|
73
|
+
* after the answer and nothing they said is an objection. Pure
|
|
74
|
+
* acknowledgements are not stored in the discussion (they accept at once), so
|
|
75
|
+
* a later entry is a follow-up the agent answers in the thread, not a
|
|
76
|
+
* complaint about the finished work. Notes written by Crewly itself do not
|
|
77
|
+
* count.
|
|
78
|
+
*
|
|
79
|
+
* @param ticket - The ticket in 待验收
|
|
80
|
+
* @returns True when silence-about-the-work can be taken as acceptance now
|
|
81
|
+
*/
|
|
82
|
+
export function ownerMovedOn(ticket) {
|
|
83
|
+
const since = ticket.submittedAt ? Date.parse(ticket.submittedAt) : NaN;
|
|
84
|
+
if (!Number.isFinite(since))
|
|
85
|
+
return false;
|
|
86
|
+
const later = (ticket.discussion ?? []).filter((d) => d.author !== TICKET_CONSTANTS.STALE.NOTE_AUTHOR &&
|
|
87
|
+
d.author !== TICKET_CONSTANTS.REVIEW.AUTO_ACCEPT_NOTE_AUTHOR &&
|
|
88
|
+
Date.parse(d.at) > since);
|
|
89
|
+
return later.length > 0 && !later.some((d) => isOwnerObjection(d.text));
|
|
90
|
+
}
|
|
58
91
|
/**
|
|
59
92
|
* The last time anything happened on a ticket: created, updated, answered,
|
|
60
93
|
* submitted or discussed.
|