@deftai/directive-core 0.95.0 → 0.96.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/dist/cache/archive.d.ts +134 -0
- package/dist/cache/archive.js +630 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.js +1 -0
- package/dist/cache/main.js +298 -1
- package/dist/content-contracts/skills/helpers.d.ts +1 -1
- package/dist/content-contracts/skills/helpers.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/hygiene.d.ts +1 -1
- package/dist/init-deposit/hygiene.js +16 -4
- package/dist/init-deposit/scaffold.js +6 -5
- package/dist/parent-turn-shape/evaluate.d.ts +84 -0
- package/dist/parent-turn-shape/evaluate.js +353 -0
- package/dist/parent-turn-shape/index.d.ts +8 -0
- package/dist/parent-turn-shape/index.js +8 -0
- package/dist/review-monitor/constants.js +3 -2
- package/dist/review-monitor/tier-detection.d.ts +7 -3
- package/dist/review-monitor/tier-detection.js +18 -1
- package/dist/review-monitor/verify.js +18 -0
- package/dist/scope/index.d.ts +2 -0
- package/dist/scope/index.js +2 -0
- package/dist/scope/main.d.ts +10 -0
- package/dist/scope/main.js +109 -24
- package/dist/scope/promote-from-issue.d.ts +49 -0
- package/dist/scope/promote-from-issue.js +367 -0
- package/dist/scope/promote-path.d.ts +39 -0
- package/dist/scope/promote-path.js +105 -0
- package/dist/swarm/routing.d.ts +4 -2
- package/dist/swarm/routing.js +26 -4
- package/dist/triage/actions/index.js +62 -2
- package/dist/triage/actions/types.d.ts +8 -1
- package/dist/triage/author-filter.d.ts +51 -0
- package/dist/triage/author-filter.js +152 -0
- package/dist/triage/classify/index.d.ts +2 -2
- package/dist/triage/classify/index.js +2 -2
- package/dist/triage/classify/label-mirror.d.ts +68 -5
- package/dist/triage/classify/label-mirror.js +261 -31
- package/dist/triage/help/registry-data.d.ts +49 -38
- package/dist/triage/help/registry-data.js +115 -40
- package/dist/triage/index.d.ts +1 -0
- package/dist/triage/index.js +1 -0
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/render.d.ts +2 -0
- package/dist/triage/queue/render.js +6 -0
- package/package.json +7 -3
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tier-1 deterministic SCM label mirror (#1423 Wave 1).
|
|
2
|
+
* Tier-1 deterministic SCM label mirror (#1423 Wave 1 + Wave 2 bootstrap).
|
|
3
3
|
*
|
|
4
4
|
* Classifies cached issues with the existing #1129 engine, then mirrors the
|
|
5
5
|
* outcome as SCM labels (dry-run default, --apply to write). Never accepts into
|
|
6
6
|
* the xBRIEF lifecycle and never writes proposed/ scopes.
|
|
7
7
|
*
|
|
8
|
+
* Wave 2 (#3125): open-only default, operator digest (totals + by state/rule/action
|
|
9
|
+
* + samples), batched rate-limit-aware apply. Bootstrap mass-triage entrypoint is
|
|
10
|
+
* `triage:classify -- --mirror` with these filters (not triage:accept).
|
|
11
|
+
*
|
|
8
12
|
* Intentionally does NOT import from ./index.js (SLizard P1 cycle). The classify
|
|
9
13
|
* engine is injected via LabelMirrorEngine / mirrorLabels() wrapper in index.ts.
|
|
10
14
|
*/
|
|
11
15
|
import type { LabelClient } from "../../vbrief-reconcile/types.js";
|
|
16
|
+
import { type AuthorFilter } from "../author-filter.js";
|
|
12
17
|
export declare const DEFAULT_IDEMPOTENCY_LABEL = "triaged";
|
|
13
18
|
export declare const CACHE_DIR_NAME = ".deft-cache";
|
|
14
19
|
export declare const CACHE_SOURCE = "github-issue";
|
|
20
|
+
/** Default apply batch size for rate-limit awareness (#3125). */
|
|
21
|
+
export declare const DEFAULT_APPLY_BATCH_SIZE = 10;
|
|
22
|
+
/** Default delay between apply batches in ms (#3125). */
|
|
23
|
+
export declare const DEFAULT_APPLY_DELAY_MS = 1000;
|
|
24
|
+
/** Default sample count in human digest (#3125). */
|
|
25
|
+
export declare const DEFAULT_DIGEST_SAMPLE_LIMIT = 15;
|
|
15
26
|
export type ClassifyAction = "defer" | "archive" | "escalate" | "accept";
|
|
16
27
|
/** Minimal issue shape used by the mirror (matches classify GitHubIssue). */
|
|
17
28
|
export interface MirrorGitHubIssue {
|
|
@@ -73,10 +84,12 @@ export interface ResolvedLabelMirrorPolicy {
|
|
|
73
84
|
readonly alwaysLabels: readonly string[];
|
|
74
85
|
readonly actionLabels: Readonly<Partial<Record<ClassifyAction, readonly string[]>>>;
|
|
75
86
|
}
|
|
76
|
-
export type LabelMirrorStatus = "planned" | "applied" | "unchanged" | "skipped_already_triaged" | "skipped_no_match" | "skipped_unreadable" | "skipped_disabled" | "error";
|
|
87
|
+
export type LabelMirrorStatus = "planned" | "applied" | "unchanged" | "skipped_already_triaged" | "skipped_no_match" | "skipped_unreadable" | "skipped_closed" | "skipped_author" | "skipped_disabled" | "error";
|
|
77
88
|
export interface LabelMirrorItem {
|
|
78
89
|
readonly repo: string;
|
|
79
90
|
readonly issue_number: number;
|
|
91
|
+
/** Issue state from cache (open/closed/unknown). */
|
|
92
|
+
readonly state: string | null;
|
|
80
93
|
readonly action: string | null;
|
|
81
94
|
readonly reason: string | null;
|
|
82
95
|
readonly ruleKind: string | null;
|
|
@@ -86,6 +99,24 @@ export interface LabelMirrorItem {
|
|
|
86
99
|
readonly status: LabelMirrorStatus;
|
|
87
100
|
readonly message?: string;
|
|
88
101
|
}
|
|
102
|
+
/** Operator digest aggregates for bootstrap mass-triage (#3125 / #1423 Wave 2). */
|
|
103
|
+
export interface LabelMirrorDigest {
|
|
104
|
+
readonly by_state: Readonly<Record<string, number>>;
|
|
105
|
+
readonly by_rule: Readonly<Record<string, number>>;
|
|
106
|
+
readonly by_action: Readonly<Record<string, number>>;
|
|
107
|
+
readonly samples: readonly LabelMirrorItem[];
|
|
108
|
+
readonly sample_limit: number;
|
|
109
|
+
readonly sample_truncated: boolean;
|
|
110
|
+
}
|
|
111
|
+
export interface LabelMirrorFilters {
|
|
112
|
+
/** When false (default), closed issues are skipped before classify. */
|
|
113
|
+
readonly include_closed: boolean;
|
|
114
|
+
readonly repo: string | null;
|
|
115
|
+
/** Active author allow-list display (#3129); null when no author filter. */
|
|
116
|
+
readonly author: string | null;
|
|
117
|
+
/** Resolved author logins for machine consumers. */
|
|
118
|
+
readonly author_logins: readonly string[] | null;
|
|
119
|
+
}
|
|
89
120
|
export interface LabelMirrorOutcome {
|
|
90
121
|
readonly project_root: string;
|
|
91
122
|
readonly dry_run: boolean;
|
|
@@ -96,10 +127,20 @@ export interface LabelMirrorOutcome {
|
|
|
96
127
|
readonly skipped_already_triaged: number;
|
|
97
128
|
readonly skipped_no_match: number;
|
|
98
129
|
readonly skipped_unreadable: number;
|
|
130
|
+
/** Closed issues skipped by open-only default (#3125). */
|
|
131
|
+
readonly skipped_closed: number;
|
|
132
|
+
/** Issues skipped by --author filter (#3129). */
|
|
133
|
+
readonly skipped_author: number;
|
|
99
134
|
readonly errors: number;
|
|
135
|
+
readonly filters: LabelMirrorFilters;
|
|
136
|
+
readonly digest: LabelMirrorDigest;
|
|
100
137
|
readonly items: readonly LabelMirrorItem[];
|
|
101
138
|
readonly policy: ResolvedLabelMirrorPolicy;
|
|
139
|
+
/** Apply path: successful writes in this run (same as applied). */
|
|
140
|
+
readonly batch_size?: number;
|
|
141
|
+
readonly delay_ms?: number;
|
|
102
142
|
}
|
|
143
|
+
export type LabelMirrorSleepFn = (ms: number) => void;
|
|
103
144
|
export interface LabelMirrorOptions {
|
|
104
145
|
readonly dryRun?: boolean;
|
|
105
146
|
readonly repo?: string | null;
|
|
@@ -110,6 +151,24 @@ export interface LabelMirrorOptions {
|
|
|
110
151
|
/** Prefer live SCM labels when true (default: !dryRun). Cache labels used for dry-run. */
|
|
111
152
|
readonly useLiveLabels?: boolean;
|
|
112
153
|
readonly now?: Date;
|
|
154
|
+
/**
|
|
155
|
+
* Include closed issues in classify+mirror. Default false (open-only) for safe
|
|
156
|
+
* bootstrap mass-triage (#3125). Opt in with CLI `--include-closed`.
|
|
157
|
+
*/
|
|
158
|
+
readonly includeClosed?: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Resolved author filter applied before plan/apply walk (#3129).
|
|
161
|
+
* Composes with open-only (AND). CLI resolves `@me` before passing this.
|
|
162
|
+
*/
|
|
163
|
+
readonly authorFilter?: AuthorFilter | null;
|
|
164
|
+
/** Max planned/applied samples in human digest (default 15). */
|
|
165
|
+
readonly sampleLimit?: number;
|
|
166
|
+
/** SCM writes per batch before delay (default 10; apply path only). */
|
|
167
|
+
readonly batchSize?: number;
|
|
168
|
+
/** Delay in ms between apply batches (default 1000; apply path only). */
|
|
169
|
+
readonly delayMs?: number;
|
|
170
|
+
/** Injectable sleep for tests (receives ms). Default busy-wait when delayMs > 0. */
|
|
171
|
+
readonly sleepMs?: LabelMirrorSleepFn;
|
|
113
172
|
/** Required: classify engine (provided by classify/index mirrorLabels wrapper). */
|
|
114
173
|
readonly engine: LabelMirrorEngine;
|
|
115
174
|
}
|
|
@@ -128,15 +187,19 @@ export declare function resolveLabelMirrorPolicy(options?: {
|
|
|
128
187
|
}): ResolvedLabelMirrorPolicy;
|
|
129
188
|
/** Labels to apply for a classified action (always + action-mapped). */
|
|
130
189
|
export declare function desiredLabelsForClassification(action: string, policy: ResolvedLabelMirrorPolicy): string[];
|
|
190
|
+
/** Build digest aggregates + samples from mirror items (#3125). */
|
|
191
|
+
export declare function buildLabelMirrorDigest(items: readonly LabelMirrorItem[], sampleLimit?: number): LabelMirrorDigest;
|
|
131
192
|
/**
|
|
132
|
-
* Run Tier-1 label mirror over the github-issue cache.
|
|
193
|
+
* Run Tier-1 label mirror over the github-issue cache (bootstrap mass-triage surface).
|
|
133
194
|
* Dry-run by default (no SCM writes). Pass dryRun: false to apply.
|
|
195
|
+
* Default state filter is open-only (#3125); pass includeClosed: true for archive stamps.
|
|
134
196
|
* Requires options.engine (classify/index wrapper injects it).
|
|
197
|
+
* Never calls triage:accept / never writes proposed/ xBRIEFs.
|
|
135
198
|
*/
|
|
136
199
|
export declare function mirrorLabels(projectRoot: string, options: LabelMirrorOptions): [number, LabelMirrorOutcome];
|
|
137
|
-
/** Human-readable digest for dry-run / apply reports. */
|
|
200
|
+
/** Human-readable digest for dry-run / apply reports (bootstrap mass-triage UX). */
|
|
138
201
|
export declare function renderLabelMirrorReport(outcome: LabelMirrorOutcome): string;
|
|
139
|
-
/** JSON-serializable outcome
|
|
202
|
+
/** JSON-serializable outcome including Wave 2 digest aggregates. */
|
|
140
203
|
export declare function labelMirrorOutcomeToJson(outcome: LabelMirrorOutcome): Record<string, unknown>;
|
|
141
204
|
/** Validate triageLabelMirror on a plan object (vbrief_validate hook). */
|
|
142
205
|
export declare function validateTriageLabelMirrorOnPlan(plan: unknown, filepath: string): string[];
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tier-1 deterministic SCM label mirror (#1423 Wave 1).
|
|
2
|
+
* Tier-1 deterministic SCM label mirror (#1423 Wave 1 + Wave 2 bootstrap).
|
|
3
3
|
*
|
|
4
4
|
* Classifies cached issues with the existing #1129 engine, then mirrors the
|
|
5
5
|
* outcome as SCM labels (dry-run default, --apply to write). Never accepts into
|
|
6
6
|
* the xBRIEF lifecycle and never writes proposed/ scopes.
|
|
7
7
|
*
|
|
8
|
+
* Wave 2 (#3125): open-only default, operator digest (totals + by state/rule/action
|
|
9
|
+
* + samples), batched rate-limit-aware apply. Bootstrap mass-triage entrypoint is
|
|
10
|
+
* `triage:classify -- --mirror` with these filters (not triage:accept).
|
|
11
|
+
*
|
|
8
12
|
* Intentionally does NOT import from ./index.js (SLizard P1 cycle). The classify
|
|
9
13
|
* engine is injected via LabelMirrorEngine / mirrorLabels() wrapper in index.ts.
|
|
10
14
|
*/
|
|
@@ -16,11 +20,18 @@ import { readPlanPolicy } from "../../policy/plan-extensions.js";
|
|
|
16
20
|
import { ScmLabelClient } from "../../vbrief-reconcile/labels.js";
|
|
17
21
|
import { isRepoMutationAllowed } from "../../vbrief-reconcile/repo-guard.js";
|
|
18
22
|
import { latestDecisions, readAuditLog } from "../actions/candidates-log.js";
|
|
23
|
+
import { authorLoginFromRawIssue, matchesAuthorFilter, } from "../author-filter.js";
|
|
19
24
|
import { resolveCandidatesLogPath } from "../cache-path.js";
|
|
20
25
|
import { iterCachedIssues } from "../summary/index.js";
|
|
21
26
|
export const DEFAULT_IDEMPOTENCY_LABEL = "triaged";
|
|
22
27
|
export const CACHE_DIR_NAME = ".deft-cache";
|
|
23
28
|
export const CACHE_SOURCE = "github-issue";
|
|
29
|
+
/** Default apply batch size for rate-limit awareness (#3125). */
|
|
30
|
+
export const DEFAULT_APPLY_BATCH_SIZE = 10;
|
|
31
|
+
/** Default delay between apply batches in ms (#3125). */
|
|
32
|
+
export const DEFAULT_APPLY_DELAY_MS = 1000;
|
|
33
|
+
/** Default sample count in human digest (#3125). */
|
|
34
|
+
export const DEFAULT_DIGEST_SAMPLE_LIMIT = 15;
|
|
24
35
|
const VALID_ACTIONS = new Set(["defer", "archive", "escalate", "accept"]);
|
|
25
36
|
/**
|
|
26
37
|
* Repo-qualified xBRIEF github-issue keys (`owner/name\\0number`).
|
|
@@ -308,10 +319,72 @@ function loadLatestDecisionMap(projectRoot) {
|
|
|
308
319
|
function decisionMapHas(map, repo, issueNumber) {
|
|
309
320
|
return map.has(`${repo}\0${issueNumber}`);
|
|
310
321
|
}
|
|
322
|
+
function defaultSleepMs(ms) {
|
|
323
|
+
if (ms <= 0) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
// Sync sleep: LabelClient.apply is sync; keep mirrorLabels non-async (#3125).
|
|
327
|
+
const sab = new SharedArrayBuffer(4);
|
|
328
|
+
const ia = new Int32Array(sab);
|
|
329
|
+
Atomics.wait(ia, 0, 0, ms);
|
|
330
|
+
}
|
|
331
|
+
function normalizeIssueState(raw) {
|
|
332
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
return raw.trim().toLowerCase();
|
|
336
|
+
}
|
|
337
|
+
function isClosedState(state) {
|
|
338
|
+
return state === "closed";
|
|
339
|
+
}
|
|
340
|
+
/** Build digest aggregates + samples from mirror items (#3125). */
|
|
341
|
+
export function buildLabelMirrorDigest(items, sampleLimit = DEFAULT_DIGEST_SAMPLE_LIMIT) {
|
|
342
|
+
const limit = Number.isFinite(sampleLimit) && sampleLimit >= 0
|
|
343
|
+
? Math.floor(sampleLimit)
|
|
344
|
+
: DEFAULT_DIGEST_SAMPLE_LIMIT;
|
|
345
|
+
const byState = {};
|
|
346
|
+
const byRule = {};
|
|
347
|
+
const byAction = {};
|
|
348
|
+
const writeItems = items.filter((i) => i.status === "planned" || i.status === "applied");
|
|
349
|
+
for (const item of writeItems) {
|
|
350
|
+
const st = item.state ?? "unknown";
|
|
351
|
+
byState[st] = (byState[st] ?? 0) + 1;
|
|
352
|
+
const rule = item.ruleKind ?? "(none)";
|
|
353
|
+
byRule[rule] = (byRule[rule] ?? 0) + 1;
|
|
354
|
+
const action = item.action ?? "(none)";
|
|
355
|
+
byAction[action] = (byAction[action] ?? 0) + 1;
|
|
356
|
+
}
|
|
357
|
+
const samples = writeItems.slice(0, limit);
|
|
358
|
+
return {
|
|
359
|
+
by_state: byState,
|
|
360
|
+
by_rule: byRule,
|
|
361
|
+
by_action: byAction,
|
|
362
|
+
samples,
|
|
363
|
+
sample_limit: limit,
|
|
364
|
+
sample_truncated: writeItems.length > limit,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function formatMissingLabelHint(message, labels) {
|
|
368
|
+
const lower = message.toLowerCase();
|
|
369
|
+
const looksMissing = lower.includes("not found") ||
|
|
370
|
+
lower.includes("could not add label") ||
|
|
371
|
+
lower.includes("invalid label") ||
|
|
372
|
+
lower.includes("unknown label") ||
|
|
373
|
+
(lower.includes("label") && (lower.includes("404") || lower.includes("does not exist")));
|
|
374
|
+
if (!looksMissing) {
|
|
375
|
+
return message;
|
|
376
|
+
}
|
|
377
|
+
const want = labels.length > 0 ? labels.join(", ") : "triaged";
|
|
378
|
+
return (`${message} — ensure label(s) exist on the repo before --apply ` +
|
|
379
|
+
`(create missing labels e.g. \`gh label create "${want.split(",")[0]?.trim() ?? "triaged"}"\`; ` +
|
|
380
|
+
`idempotency + actionLabels must exist or apply fails closed per issue).`);
|
|
381
|
+
}
|
|
311
382
|
/**
|
|
312
|
-
* Run Tier-1 label mirror over the github-issue cache.
|
|
383
|
+
* Run Tier-1 label mirror over the github-issue cache (bootstrap mass-triage surface).
|
|
313
384
|
* Dry-run by default (no SCM writes). Pass dryRun: false to apply.
|
|
385
|
+
* Default state filter is open-only (#3125); pass includeClosed: true for archive stamps.
|
|
314
386
|
* Requires options.engine (classify/index wrapper injects it).
|
|
387
|
+
* Never calls triage:accept / never writes proposed/ xBRIEFs.
|
|
315
388
|
*/
|
|
316
389
|
export function mirrorLabels(projectRoot, options) {
|
|
317
390
|
const root = resolve(projectRoot);
|
|
@@ -321,12 +394,39 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
321
394
|
const useLiveLabels = options.useLiveLabels ?? !dryRun;
|
|
322
395
|
const client = options.client ?? (useLiveLabels || !dryRun ? new ScmLabelClient() : undefined);
|
|
323
396
|
const engine = options.engine;
|
|
397
|
+
const includeClosed = options.includeClosed === true;
|
|
398
|
+
const authorFilter = options.authorFilter !== undefined && options.authorFilter !== null
|
|
399
|
+
? options.authorFilter
|
|
400
|
+
: null;
|
|
401
|
+
const sampleLimit = options.sampleLimit ?? DEFAULT_DIGEST_SAMPLE_LIMIT;
|
|
402
|
+
const batchSize = options.batchSize !== undefined
|
|
403
|
+
? Math.max(1, Math.floor(options.batchSize))
|
|
404
|
+
: DEFAULT_APPLY_BATCH_SIZE;
|
|
405
|
+
const delayMs = options.delayMs !== undefined && options.delayMs >= 0
|
|
406
|
+
? Math.floor(options.delayMs)
|
|
407
|
+
: dryRun
|
|
408
|
+
? 0
|
|
409
|
+
: DEFAULT_APPLY_DELAY_MS;
|
|
410
|
+
const sleepMs = options.sleepMs ?? defaultSleepMs;
|
|
411
|
+
const repoFilter = options.repo !== undefined && options.repo !== null && options.repo.trim().length > 0
|
|
412
|
+
? options.repo.trim()
|
|
413
|
+
: null;
|
|
414
|
+
const filters = {
|
|
415
|
+
include_closed: includeClosed,
|
|
416
|
+
repo: repoFilter,
|
|
417
|
+
author: authorFilter !== null ? authorFilter.display : null,
|
|
418
|
+
author_logins: authorFilter !== null ? authorFilter.allowLogins : null,
|
|
419
|
+
};
|
|
324
420
|
const items = [];
|
|
325
421
|
const outcomeBase = {
|
|
326
422
|
project_root: root,
|
|
327
423
|
dry_run: dryRun,
|
|
328
424
|
policy,
|
|
425
|
+
filters,
|
|
426
|
+
batch_size: dryRun ? undefined : batchSize,
|
|
427
|
+
delay_ms: dryRun ? undefined : delayMs,
|
|
329
428
|
};
|
|
429
|
+
const emptyDigest = buildLabelMirrorDigest([], sampleLimit);
|
|
330
430
|
if (!policy.enabled) {
|
|
331
431
|
return [
|
|
332
432
|
0,
|
|
@@ -339,11 +439,15 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
339
439
|
skipped_already_triaged: 0,
|
|
340
440
|
skipped_no_match: 0,
|
|
341
441
|
skipped_unreadable: 0,
|
|
442
|
+
skipped_closed: 0,
|
|
443
|
+
skipped_author: 0,
|
|
342
444
|
errors: 0,
|
|
445
|
+
digest: emptyDigest,
|
|
343
446
|
items: [
|
|
344
447
|
{
|
|
345
448
|
repo: "",
|
|
346
449
|
issue_number: 0,
|
|
450
|
+
state: null,
|
|
347
451
|
action: null,
|
|
348
452
|
reason: null,
|
|
349
453
|
ruleKind: null,
|
|
@@ -365,8 +469,8 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
365
469
|
const decisions = loadLatestDecisionMap(root);
|
|
366
470
|
const now = options.now ?? new Date();
|
|
367
471
|
let pairs = iterCachedIssues(cacheRoot);
|
|
368
|
-
if (
|
|
369
|
-
const want =
|
|
472
|
+
if (repoFilter !== null) {
|
|
473
|
+
const want = repoFilter.toLowerCase();
|
|
370
474
|
pairs = pairs.filter(([repo]) => repo.toLowerCase() === want);
|
|
371
475
|
}
|
|
372
476
|
let planned = 0;
|
|
@@ -375,7 +479,10 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
375
479
|
let skippedAlready = 0;
|
|
376
480
|
let skippedNoMatch = 0;
|
|
377
481
|
let skippedUnreadable = 0;
|
|
482
|
+
let skippedClosed = 0;
|
|
483
|
+
let skippedAuthor = 0;
|
|
378
484
|
let errors = 0;
|
|
485
|
+
let applyWritesSinceSleep = 0;
|
|
379
486
|
for (const [repo, issueNumber] of pairs) {
|
|
380
487
|
const issue = readCachedRawIssue(cacheRoot, repo, issueNumber);
|
|
381
488
|
if (issue === null) {
|
|
@@ -383,6 +490,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
383
490
|
items.push({
|
|
384
491
|
repo,
|
|
385
492
|
issue_number: issueNumber,
|
|
493
|
+
state: null,
|
|
386
494
|
action: null,
|
|
387
495
|
reason: null,
|
|
388
496
|
ruleKind: null,
|
|
@@ -394,6 +502,48 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
394
502
|
});
|
|
395
503
|
continue;
|
|
396
504
|
}
|
|
505
|
+
const state = normalizeIssueState(issue.state);
|
|
506
|
+
// Open-only default: skip closed before classify (avoids mass-stamping archive).
|
|
507
|
+
if (!includeClosed && isClosedState(state)) {
|
|
508
|
+
skippedClosed += 1;
|
|
509
|
+
items.push({
|
|
510
|
+
repo,
|
|
511
|
+
issue_number: issueNumber,
|
|
512
|
+
state,
|
|
513
|
+
action: null,
|
|
514
|
+
reason: null,
|
|
515
|
+
ruleKind: null,
|
|
516
|
+
current: issueLabelNames(issue).sort(),
|
|
517
|
+
desired: [],
|
|
518
|
+
add: [],
|
|
519
|
+
status: "skipped_closed",
|
|
520
|
+
message: "open-only default; pass includeClosed / --include-closed to mirror closed issues",
|
|
521
|
+
});
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
// Author filter (#3129): AND with open-only / other filters; missing author = non-match.
|
|
525
|
+
if (authorFilter !== null) {
|
|
526
|
+
const login = authorLoginFromRawIssue(issue);
|
|
527
|
+
if (!matchesAuthorFilter(login, authorFilter)) {
|
|
528
|
+
skippedAuthor += 1;
|
|
529
|
+
items.push({
|
|
530
|
+
repo,
|
|
531
|
+
issue_number: issueNumber,
|
|
532
|
+
state,
|
|
533
|
+
action: null,
|
|
534
|
+
reason: null,
|
|
535
|
+
ruleKind: null,
|
|
536
|
+
current: issueLabelNames(issue).sort(),
|
|
537
|
+
desired: [],
|
|
538
|
+
add: [],
|
|
539
|
+
status: "skipped_author",
|
|
540
|
+
message: login === null
|
|
541
|
+
? `author filter ${authorFilter.display}: missing author on cache row (unknown — excluded)`
|
|
542
|
+
: `author filter ${authorFilter.display}: author.login=${login} does not match`,
|
|
543
|
+
});
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
397
547
|
let current;
|
|
398
548
|
if (useLiveLabels && client !== undefined) {
|
|
399
549
|
try {
|
|
@@ -406,6 +556,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
406
556
|
items.push({
|
|
407
557
|
repo,
|
|
408
558
|
issue_number: issueNumber,
|
|
559
|
+
state,
|
|
409
560
|
action: null,
|
|
410
561
|
reason: null,
|
|
411
562
|
ruleKind: null,
|
|
@@ -427,6 +578,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
427
578
|
items.push({
|
|
428
579
|
repo,
|
|
429
580
|
issue_number: issueNumber,
|
|
581
|
+
state,
|
|
430
582
|
action: null,
|
|
431
583
|
reason: null,
|
|
432
584
|
ruleKind: null,
|
|
@@ -455,6 +607,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
455
607
|
items.push({
|
|
456
608
|
repo,
|
|
457
609
|
issue_number: issueNumber,
|
|
610
|
+
state,
|
|
458
611
|
action: null,
|
|
459
612
|
reason: null,
|
|
460
613
|
ruleKind: null,
|
|
@@ -473,6 +626,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
473
626
|
items.push({
|
|
474
627
|
repo,
|
|
475
628
|
issue_number: issueNumber,
|
|
629
|
+
state,
|
|
476
630
|
action: classification.action,
|
|
477
631
|
reason: classification.reason,
|
|
478
632
|
ruleKind: classification.ruleKind,
|
|
@@ -488,6 +642,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
488
642
|
items.push({
|
|
489
643
|
repo,
|
|
490
644
|
issue_number: issueNumber,
|
|
645
|
+
state,
|
|
491
646
|
action: classification.action,
|
|
492
647
|
reason: classification.reason,
|
|
493
648
|
ruleKind: classification.ruleKind,
|
|
@@ -498,7 +653,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
498
653
|
});
|
|
499
654
|
continue;
|
|
500
655
|
}
|
|
501
|
-
// --apply path: SCM boundary + write
|
|
656
|
+
// --apply path: SCM boundary + write (batched + delay for rate-limit awareness)
|
|
502
657
|
const mutateGate = isRepoMutationAllowed(repo, root, {
|
|
503
658
|
allowCrossRepo: options.allowCrossRepo,
|
|
504
659
|
allowlist: options.repoAllowlist,
|
|
@@ -509,6 +664,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
509
664
|
items.push({
|
|
510
665
|
repo,
|
|
511
666
|
issue_number: issueNumber,
|
|
667
|
+
state,
|
|
512
668
|
action: classification.action,
|
|
513
669
|
reason: classification.reason,
|
|
514
670
|
ruleKind: classification.ruleKind,
|
|
@@ -525,6 +681,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
525
681
|
items.push({
|
|
526
682
|
repo,
|
|
527
683
|
issue_number: issueNumber,
|
|
684
|
+
state,
|
|
528
685
|
action: classification.action,
|
|
529
686
|
reason: classification.reason,
|
|
530
687
|
ruleKind: classification.ruleKind,
|
|
@@ -537,11 +694,18 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
537
694
|
continue;
|
|
538
695
|
}
|
|
539
696
|
try {
|
|
697
|
+
// Count every SCM write *attempt* toward the batch (including failures) so
|
|
698
|
+
// rate-limit delay still applies under partial failure storms (#3125 Greptile P1).
|
|
699
|
+
if (applyWritesSinceSleep > 0 && applyWritesSinceSleep % batchSize === 0 && delayMs > 0) {
|
|
700
|
+
sleepMs(delayMs);
|
|
701
|
+
}
|
|
702
|
+
applyWritesSinceSleep += 1;
|
|
540
703
|
client.apply(repo, issueNumber, add, []);
|
|
541
704
|
applied += 1;
|
|
542
705
|
items.push({
|
|
543
706
|
repo,
|
|
544
707
|
issue_number: issueNumber,
|
|
708
|
+
state,
|
|
545
709
|
action: classification.action,
|
|
546
710
|
reason: classification.reason,
|
|
547
711
|
ruleKind: classification.ruleKind,
|
|
@@ -553,9 +717,11 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
553
717
|
}
|
|
554
718
|
catch (exc) {
|
|
555
719
|
errors += 1;
|
|
720
|
+
const rawMsg = exc instanceof Error ? exc.message : String(exc);
|
|
556
721
|
items.push({
|
|
557
722
|
repo,
|
|
558
723
|
issue_number: issueNumber,
|
|
724
|
+
state,
|
|
559
725
|
action: classification.action,
|
|
560
726
|
reason: classification.reason,
|
|
561
727
|
ruleKind: classification.ruleKind,
|
|
@@ -563,10 +729,12 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
563
729
|
desired,
|
|
564
730
|
add,
|
|
565
731
|
status: "error",
|
|
566
|
-
message:
|
|
732
|
+
message: formatMissingLabelHint(rawMsg, add),
|
|
567
733
|
});
|
|
734
|
+
// Partial failure: continue remaining issues (idempotent re-run skips applied).
|
|
568
735
|
}
|
|
569
736
|
}
|
|
737
|
+
const digest = buildLabelMirrorDigest(items, sampleLimit);
|
|
570
738
|
const outcome = {
|
|
571
739
|
...outcomeBase,
|
|
572
740
|
scanned: pairs.length,
|
|
@@ -576,35 +744,75 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
576
744
|
skipped_already_triaged: skippedAlready,
|
|
577
745
|
skipped_no_match: skippedNoMatch,
|
|
578
746
|
skipped_unreadable: skippedUnreadable,
|
|
747
|
+
skipped_closed: skippedClosed,
|
|
748
|
+
skipped_author: skippedAuthor,
|
|
579
749
|
errors,
|
|
750
|
+
digest,
|
|
580
751
|
items,
|
|
581
752
|
};
|
|
582
753
|
return [errors > 0 ? 1 : 0, outcome];
|
|
583
754
|
}
|
|
584
|
-
|
|
755
|
+
function formatCountMap(map) {
|
|
756
|
+
const keys = Object.keys(map).sort();
|
|
757
|
+
if (keys.length === 0) {
|
|
758
|
+
return [" (none)"];
|
|
759
|
+
}
|
|
760
|
+
return keys.map((k) => ` ${k}: ${map[k]}`);
|
|
761
|
+
}
|
|
762
|
+
/** Human-readable digest for dry-run / apply reports (bootstrap mass-triage UX). */
|
|
585
763
|
export function renderLabelMirrorReport(outcome) {
|
|
586
764
|
const lines = [];
|
|
587
765
|
const mode = outcome.dry_run ? "dry-run" : "apply";
|
|
588
|
-
lines.push(`triage:classify --mirror (${mode})`);
|
|
766
|
+
lines.push(`triage:classify --mirror (${mode}) — bootstrap mass-triage (#1423 Wave 2)`);
|
|
767
|
+
const stateFilter = outcome.filters.include_closed ? "all (include-closed)" : "open-only";
|
|
768
|
+
const repoPart = outcome.filters.repo ?? "*";
|
|
769
|
+
const authorPart = outcome.filters.author ?? "*";
|
|
770
|
+
lines.push(`filters: state=${stateFilter} repo=${repoPart} author=${authorPart}`);
|
|
589
771
|
lines.push(`scanned=${outcome.scanned} planned=${outcome.planned} applied=${outcome.applied} ` +
|
|
590
772
|
`unchanged=${outcome.unchanged} already_triaged=${outcome.skipped_already_triaged} ` +
|
|
591
|
-
`no_match=${outcome.skipped_no_match}
|
|
592
|
-
`
|
|
773
|
+
`no_match=${outcome.skipped_no_match} closed_skipped=${outcome.skipped_closed} ` +
|
|
774
|
+
`author_skipped=${outcome.skipped_author} ` +
|
|
775
|
+
`unreadable=${outcome.skipped_unreadable} errors=${outcome.errors}`);
|
|
593
776
|
lines.push(`idempotencyLabel=${outcome.policy.idempotencyLabel} alwaysLabels=${JSON.stringify(outcome.policy.alwaysLabels)}`);
|
|
777
|
+
if (!outcome.dry_run) {
|
|
778
|
+
lines.push(`apply: batch_size=${outcome.batch_size ?? DEFAULT_APPLY_BATCH_SIZE} delay_ms=${outcome.delay_ms ?? DEFAULT_APPLY_DELAY_MS}`);
|
|
779
|
+
}
|
|
594
780
|
lines.push("");
|
|
595
|
-
|
|
596
|
-
lines.push(outcome.
|
|
597
|
-
|
|
781
|
+
lines.push("By state (planned/applied):");
|
|
782
|
+
lines.push(...formatCountMap(outcome.digest.by_state));
|
|
783
|
+
lines.push("By rule (planned/applied):");
|
|
784
|
+
lines.push(...formatCountMap(outcome.digest.by_rule));
|
|
785
|
+
lines.push("By action (planned/applied):");
|
|
786
|
+
lines.push(...formatCountMap(outcome.digest.by_action));
|
|
787
|
+
lines.push("");
|
|
788
|
+
const writeTotal = outcome.planned + outcome.applied;
|
|
789
|
+
lines.push(outcome.dry_run
|
|
790
|
+
? `Samples (up to ${outcome.digest.sample_limit} of ${writeTotal} planned):`
|
|
791
|
+
: `Samples (up to ${outcome.digest.sample_limit} of ${writeTotal} applied/planned):`);
|
|
792
|
+
if (outcome.digest.samples.length === 0) {
|
|
598
793
|
lines.push("- none");
|
|
599
794
|
}
|
|
600
795
|
else {
|
|
601
|
-
for (const item of
|
|
796
|
+
for (const item of outcome.digest.samples) {
|
|
602
797
|
const actionPart = item.action !== null ? ` action=${item.action}` : "";
|
|
603
798
|
const rulePart = item.ruleKind !== null ? ` rule=${item.ruleKind}` : "";
|
|
799
|
+
const statePart = item.state !== null ? ` state=${item.state}` : "";
|
|
604
800
|
const addPart = sanitizeReportFragment(item.add.join(", +"));
|
|
605
|
-
lines.push(`- ${item.repo}#${item.issue_number}:${actionPart}${rulePart} +${addPart}`);
|
|
801
|
+
lines.push(`- ${item.repo}#${item.issue_number}:${statePart}${actionPart}${rulePart} +${addPart}`);
|
|
802
|
+
}
|
|
803
|
+
if (outcome.digest.sample_truncated) {
|
|
804
|
+
const remaining = writeTotal - outcome.digest.samples.length;
|
|
805
|
+
lines.push(`… and ${remaining} more (use --json for full items list)`);
|
|
606
806
|
}
|
|
607
807
|
}
|
|
808
|
+
if (outcome.skipped_closed > 0) {
|
|
809
|
+
lines.push("");
|
|
810
|
+
lines.push(`Skipped closed (open-only default): ${outcome.skipped_closed} — re-run with --include-closed to include archive`);
|
|
811
|
+
}
|
|
812
|
+
if (outcome.skipped_author > 0) {
|
|
813
|
+
lines.push("");
|
|
814
|
+
lines.push(`Skipped (author filter ${outcome.filters.author ?? "?"}): ${outcome.skipped_author}`);
|
|
815
|
+
}
|
|
608
816
|
const already = outcome.items.filter((i) => i.status === "skipped_already_triaged");
|
|
609
817
|
if (already.length > 0) {
|
|
610
818
|
lines.push("");
|
|
@@ -617,19 +825,19 @@ export function renderLabelMirrorReport(outcome) {
|
|
|
617
825
|
const errs = outcome.items.filter((i) => i.status === "error");
|
|
618
826
|
if (errs.length > 0) {
|
|
619
827
|
lines.push("");
|
|
620
|
-
lines.push(
|
|
828
|
+
lines.push(`Errors (partial failure report; ${errs.length} of ${outcome.scanned}):`);
|
|
621
829
|
for (const item of errs) {
|
|
622
830
|
const msg = sanitizeReportFragment(item.message ?? "unknown error");
|
|
623
831
|
lines.push(`- ${item.repo}#${item.issue_number}: ${msg}`);
|
|
624
832
|
}
|
|
625
833
|
}
|
|
626
|
-
if (outcome.dry_run &&
|
|
834
|
+
if (outcome.dry_run && writeTotal > 0) {
|
|
627
835
|
lines.push("");
|
|
628
|
-
lines.push("Dry-run
|
|
836
|
+
lines.push("Dry-run — re-run with --mirror --apply to write these labels via SCM (batched; never triage:accept).");
|
|
629
837
|
}
|
|
630
838
|
return `${lines.join("\n")}\n`;
|
|
631
839
|
}
|
|
632
|
-
/** JSON-serializable outcome
|
|
840
|
+
/** JSON-serializable outcome including Wave 2 digest aggregates. */
|
|
633
841
|
export function labelMirrorOutcomeToJson(outcome) {
|
|
634
842
|
return {
|
|
635
843
|
project_root: outcome.project_root,
|
|
@@ -641,25 +849,47 @@ export function labelMirrorOutcomeToJson(outcome) {
|
|
|
641
849
|
skipped_already_triaged: outcome.skipped_already_triaged,
|
|
642
850
|
skipped_no_match: outcome.skipped_no_match,
|
|
643
851
|
skipped_unreadable: outcome.skipped_unreadable,
|
|
852
|
+
skipped_closed: outcome.skipped_closed,
|
|
853
|
+
skipped_author: outcome.skipped_author,
|
|
644
854
|
errors: outcome.errors,
|
|
855
|
+
filters: {
|
|
856
|
+
include_closed: outcome.filters.include_closed,
|
|
857
|
+
repo: outcome.filters.repo,
|
|
858
|
+
author: outcome.filters.author,
|
|
859
|
+
author_logins: outcome.filters.author_logins,
|
|
860
|
+
},
|
|
861
|
+
digest: {
|
|
862
|
+
by_state: { ...outcome.digest.by_state },
|
|
863
|
+
by_rule: { ...outcome.digest.by_rule },
|
|
864
|
+
by_action: { ...outcome.digest.by_action },
|
|
865
|
+
sample_limit: outcome.digest.sample_limit,
|
|
866
|
+
sample_truncated: outcome.digest.sample_truncated,
|
|
867
|
+
samples: outcome.digest.samples.map((i) => itemToJson(i)),
|
|
868
|
+
},
|
|
869
|
+
...(outcome.batch_size !== undefined ? { batch_size: outcome.batch_size } : {}),
|
|
870
|
+
...(outcome.delay_ms !== undefined ? { delay_ms: outcome.delay_ms } : {}),
|
|
645
871
|
policy: {
|
|
646
872
|
enabled: outcome.policy.enabled,
|
|
647
873
|
idempotencyLabel: outcome.policy.idempotencyLabel,
|
|
648
874
|
alwaysLabels: [...outcome.policy.alwaysLabels],
|
|
649
875
|
actionLabels: Object.fromEntries(Object.entries(outcome.policy.actionLabels).map(([k, v]) => [k, [...(v ?? [])]])),
|
|
650
876
|
},
|
|
651
|
-
items: outcome.items.map((i) => (
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
877
|
+
items: outcome.items.map((i) => itemToJson(i)),
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function itemToJson(i) {
|
|
881
|
+
return {
|
|
882
|
+
repo: i.repo,
|
|
883
|
+
issue_number: i.issue_number,
|
|
884
|
+
state: i.state,
|
|
885
|
+
action: i.action,
|
|
886
|
+
reason: i.reason,
|
|
887
|
+
ruleKind: i.ruleKind,
|
|
888
|
+
current: [...i.current],
|
|
889
|
+
desired: [...i.desired],
|
|
890
|
+
add: [...i.add],
|
|
891
|
+
status: i.status,
|
|
892
|
+
...(i.message !== undefined ? { message: i.message } : {}),
|
|
663
893
|
};
|
|
664
894
|
}
|
|
665
895
|
/** Validate triageLabelMirror on a plan object (vbrief_validate hook). */
|