@enrichlayer/el-linear 1.41.0 → 1.41.1
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/commands/issues.js
CHANGED
|
@@ -9,7 +9,7 @@ import { formatSopParentBlock, getSopLabelGateConfig, hasSopLabel, isUnresolvabl
|
|
|
9
9
|
import { resolveDefaultStatus } from "../config/status-defaults.js";
|
|
10
10
|
import { enforceTerms } from "../config/term-enforcer.js";
|
|
11
11
|
import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, } from "../queries/issues.js";
|
|
12
|
-
import { DEFAULT_DUPLICATE_THRESHOLD, DEFAULT_HARD_BLOCK_THRESHOLD, formatDuplicateBlock, scoreDuplicateCandidates, tokenizeTitle, } from "../utils/duplicate-detection.js";
|
|
12
|
+
import { bypassesDuplicateHardBlock, DEFAULT_DUPLICATE_THRESHOLD, DEFAULT_HARD_BLOCK_THRESHOLD, formatDuplicateBlock, scoreDuplicateCandidates, tokenizeTitle, } from "../utils/duplicate-detection.js";
|
|
13
13
|
import { createFileService } from "../utils/file-service.js";
|
|
14
14
|
import { applyFooter } from "../utils/footer.js";
|
|
15
15
|
import { emitGateEvent } from "../utils/gate-telemetry.js";
|
|
@@ -589,7 +589,12 @@ async function enforceNoDuplicateIssue(title, options, issuesService) {
|
|
|
589
589
|
// The gate would hard-block. Record the decision so `el-telemetry gates`
|
|
590
590
|
// can compute override-rate (DEV-4834): `overridden` when the user passed
|
|
591
591
|
// --allow-duplicate and we proceed anyway, `blocked` when we stop creation.
|
|
592
|
-
|
|
592
|
+
//
|
|
593
|
+
// The escape set lives in `bypassesDuplicateHardBlock` rather than inline so
|
|
594
|
+
// the remedy copy in `formatDuplicateBlock` is derived from the same source
|
|
595
|
+
// this decision reads (DEV-6205 — the message previously named a flag the
|
|
596
|
+
// gate never consulted).
|
|
597
|
+
if (bypassesDuplicateHardBlock(options)) {
|
|
593
598
|
await emitGateEvent("el-linear", "issues create", {
|
|
594
599
|
...gateEvent,
|
|
595
600
|
outcome: "overridden",
|
|
@@ -73,14 +73,21 @@ export async function readIssues(issueIds, options, command) {
|
|
|
73
73
|
const rootOpts = getRootOpts(command);
|
|
74
74
|
const { graphQLService, issuesService } = await createIssuesService(rootOpts);
|
|
75
75
|
const fileService = await createFileService(rootOpts);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
// `issues` owns these options for its no-subcommand shorthand, while
|
|
77
|
+
// `issues read` registers the same options on the child command. Commander
|
|
78
|
+
// stores a duplicated flag on the parent even when it appears after `read`,
|
|
79
|
+
// so the action callback's local options can be empty. Merge the command
|
|
80
|
+
// hierarchy here, with local values winning, so every read route shares the
|
|
81
|
+
// same deterministic option contract.
|
|
82
|
+
const readOptions = { ...command.optsWithGlobals(), ...options };
|
|
83
|
+
const fieldName = typeof readOptions.field === "string" ? readOptions.field : null;
|
|
84
|
+
const bodyOnly = readOptions.body === true;
|
|
85
|
+
const sectionsRaw = typeof readOptions.sections === "string" ? readOptions.sections : null;
|
|
79
86
|
// DEV-4476: --with opt-in includes (currently `relations`). Throws on
|
|
80
87
|
// unknown values via parseWithIncludes — fail fast in the CLI per the
|
|
81
88
|
// deterministic-CLI doctrine.
|
|
82
|
-
const includes = typeof
|
|
83
|
-
? parseWithIncludes(
|
|
89
|
+
const includes = typeof readOptions.with === "string"
|
|
90
|
+
? parseWithIncludes(readOptions.with)
|
|
84
91
|
: { relations: false };
|
|
85
92
|
if (fieldName && sectionsRaw) {
|
|
86
93
|
throw new Error("--field and --sections are mutually exclusive. Use --field for a single section (plain-text output) or --sections for multiple (JSON map).");
|
|
@@ -70,6 +70,30 @@ export declare const DEFAULT_DUPLICATE_THRESHOLD = 0.35;
|
|
|
70
70
|
* Overridable via `config.validation.duplicateHardBlockThreshold`.
|
|
71
71
|
*/
|
|
72
72
|
export declare const DEFAULT_HARD_BLOCK_THRESHOLD = 0.6;
|
|
73
|
+
/**
|
|
74
|
+
* The one CLI flag that lets a HARD-BLOCKED create proceed.
|
|
75
|
+
*
|
|
76
|
+
* Exported so the remedy copy in {@link formatDuplicateBlock} is BUILT from the
|
|
77
|
+
* same constant the gate honors, rather than restating it. DEV-6205 shipped a
|
|
78
|
+
* remedy naming `--parent <id>` alone — a flag the gate never reads — so the
|
|
79
|
+
* message promised an outcome the command refused. Deriving the copy from this
|
|
80
|
+
* constant makes that class of drift a compile-time concern instead of a
|
|
81
|
+
* proofreading one.
|
|
82
|
+
*/
|
|
83
|
+
export declare const DUPLICATE_GATE_OVERRIDE_FLAG = "--allow-duplicate";
|
|
84
|
+
/**
|
|
85
|
+
* Does this parsed option set clear the duplicate gate's HARD block?
|
|
86
|
+
*
|
|
87
|
+
* The single source of truth for the escape set, consumed by the gate itself
|
|
88
|
+
* (`enforceNoDuplicateIssue`) and asserted against the rendered remedy copy by
|
|
89
|
+
* the composition test. Note `--skip-validation` also bypasses, but it returns
|
|
90
|
+
* long before scoring (it skips ALL field validation), so it is not part of the
|
|
91
|
+
* hard-block decision this predicate models and is deliberately not offered as
|
|
92
|
+
* a remedy.
|
|
93
|
+
*/
|
|
94
|
+
export declare function bypassesDuplicateHardBlock(options: {
|
|
95
|
+
allowDuplicate?: unknown;
|
|
96
|
+
}): boolean;
|
|
73
97
|
/** A scored duplicate candidate, ready to print in the block. */
|
|
74
98
|
export interface DuplicateCandidate {
|
|
75
99
|
identifier: string;
|
|
@@ -116,5 +140,30 @@ export declare function scoreDuplicateCandidates(title: string, candidates: Line
|
|
|
116
140
|
* "advisory"` is printed as a warning when the score is below the hard-block
|
|
117
141
|
* threshold: creation already proceeded, so the trailing hint differs (no
|
|
118
142
|
* "re-run" — there's nothing to re-run).
|
|
143
|
+
*
|
|
144
|
+
* Three remedies, not two (DEV-6205). "Same work → comment" and "distinct →
|
|
145
|
+
* --allow-duplicate" leave out the most common real case: the new work is a
|
|
146
|
+
* piece of the match — neither a duplicate of it nor unrelated to it. Offering
|
|
147
|
+
* only the two extremes pushes the operator toward reusing the matched issue,
|
|
148
|
+
* which is actively harmful when that issue is a multi-phase parent: the branch
|
|
149
|
+
* then carries the parent's id, and merging it auto-closes work that isn't done.
|
|
150
|
+
* That is not hypothetical — it is what the omission cost on MAR-744, where a
|
|
151
|
+
* findings pack was filed against a six-criterion parent because `--parent` was
|
|
152
|
+
* never mentioned.
|
|
153
|
+
*
|
|
154
|
+
* The sub-issue remedy is per-tier, NOT shared, because the two tiers are at
|
|
155
|
+
* opposite sides of the create:
|
|
156
|
+
*
|
|
157
|
+
* - **block** — creation was refused, so the remedy is a re-run. `--parent`
|
|
158
|
+
* alone does NOT satisfy the gate ({@link bypassesDuplicateHardBlock} reads
|
|
159
|
+
* only `allowDuplicate`), so the copy is built from
|
|
160
|
+
* {@link DUPLICATE_GATE_OVERRIDE_FLAG} and names both flags. A message that
|
|
161
|
+
* names a command the gate then refuses is the DEV-6205 bug one level down;
|
|
162
|
+
* the composition test parses this copy through the real CLI and asserts it
|
|
163
|
+
* actually clears the gate.
|
|
164
|
+
* - **advisory** — creation ALREADY proceeded, so there is nothing to re-run
|
|
165
|
+
* and a re-run would file a second issue (which then scores 1.0 against its
|
|
166
|
+
* own twin and hard-blocks). The remedy is to attach the issue that now
|
|
167
|
+
* exists, via `issues update`.
|
|
119
168
|
*/
|
|
120
169
|
export declare function formatDuplicateBlock(candidates: DuplicateCandidate[], mode?: "block" | "advisory"): string;
|
|
@@ -141,6 +141,30 @@ const BOILERPLATE_STOPWORDS = new Set([
|
|
|
141
141
|
"create",
|
|
142
142
|
"update",
|
|
143
143
|
]);
|
|
144
|
+
/**
|
|
145
|
+
* The one CLI flag that lets a HARD-BLOCKED create proceed.
|
|
146
|
+
*
|
|
147
|
+
* Exported so the remedy copy in {@link formatDuplicateBlock} is BUILT from the
|
|
148
|
+
* same constant the gate honors, rather than restating it. DEV-6205 shipped a
|
|
149
|
+
* remedy naming `--parent <id>` alone — a flag the gate never reads — so the
|
|
150
|
+
* message promised an outcome the command refused. Deriving the copy from this
|
|
151
|
+
* constant makes that class of drift a compile-time concern instead of a
|
|
152
|
+
* proofreading one.
|
|
153
|
+
*/
|
|
154
|
+
export const DUPLICATE_GATE_OVERRIDE_FLAG = "--allow-duplicate";
|
|
155
|
+
/**
|
|
156
|
+
* Does this parsed option set clear the duplicate gate's HARD block?
|
|
157
|
+
*
|
|
158
|
+
* The single source of truth for the escape set, consumed by the gate itself
|
|
159
|
+
* (`enforceNoDuplicateIssue`) and asserted against the rendered remedy copy by
|
|
160
|
+
* the composition test. Note `--skip-validation` also bypasses, but it returns
|
|
161
|
+
* long before scoring (it skips ALL field validation), so it is not part of the
|
|
162
|
+
* hard-block decision this predicate models and is deliberately not offered as
|
|
163
|
+
* a remedy.
|
|
164
|
+
*/
|
|
165
|
+
export function bypassesDuplicateHardBlock(options) {
|
|
166
|
+
return Boolean(options.allowDuplicate);
|
|
167
|
+
}
|
|
144
168
|
/**
|
|
145
169
|
* Tokenize a title into a set of salient lowercase keywords.
|
|
146
170
|
*
|
|
@@ -219,19 +243,52 @@ export function scoreDuplicateCandidates(title, candidates, threshold = DEFAULT_
|
|
|
219
243
|
* "advisory"` is printed as a warning when the score is below the hard-block
|
|
220
244
|
* threshold: creation already proceeded, so the trailing hint differs (no
|
|
221
245
|
* "re-run" — there's nothing to re-run).
|
|
246
|
+
*
|
|
247
|
+
* Three remedies, not two (DEV-6205). "Same work → comment" and "distinct →
|
|
248
|
+
* --allow-duplicate" leave out the most common real case: the new work is a
|
|
249
|
+
* piece of the match — neither a duplicate of it nor unrelated to it. Offering
|
|
250
|
+
* only the two extremes pushes the operator toward reusing the matched issue,
|
|
251
|
+
* which is actively harmful when that issue is a multi-phase parent: the branch
|
|
252
|
+
* then carries the parent's id, and merging it auto-closes work that isn't done.
|
|
253
|
+
* That is not hypothetical — it is what the omission cost on MAR-744, where a
|
|
254
|
+
* findings pack was filed against a six-criterion parent because `--parent` was
|
|
255
|
+
* never mentioned.
|
|
256
|
+
*
|
|
257
|
+
* The sub-issue remedy is per-tier, NOT shared, because the two tiers are at
|
|
258
|
+
* opposite sides of the create:
|
|
259
|
+
*
|
|
260
|
+
* - **block** — creation was refused, so the remedy is a re-run. `--parent`
|
|
261
|
+
* alone does NOT satisfy the gate ({@link bypassesDuplicateHardBlock} reads
|
|
262
|
+
* only `allowDuplicate`), so the copy is built from
|
|
263
|
+
* {@link DUPLICATE_GATE_OVERRIDE_FLAG} and names both flags. A message that
|
|
264
|
+
* names a command the gate then refuses is the DEV-6205 bug one level down;
|
|
265
|
+
* the composition test parses this copy through the real CLI and asserts it
|
|
266
|
+
* actually clears the gate.
|
|
267
|
+
* - **advisory** — creation ALREADY proceeded, so there is nothing to re-run
|
|
268
|
+
* and a re-run would file a second issue (which then scores 1.0 against its
|
|
269
|
+
* own twin and hard-blocks). The remedy is to attach the issue that now
|
|
270
|
+
* exists, via `issues update`.
|
|
222
271
|
*/
|
|
223
272
|
export function formatDuplicateBlock(candidates, mode = "block") {
|
|
224
273
|
const lines = candidates.map((c) => ` ${c.identifier} · ${c.title} · ${c.state} · ${c.assignee} (similarity ${c.score})`);
|
|
274
|
+
// Name the parent when there is exactly one candidate; with several there is
|
|
275
|
+
// no single right parent, and silently picking the top score would invite a
|
|
276
|
+
// wrong one.
|
|
277
|
+
const parentRef = candidates.length === 1 ? candidates[0].identifier : "<id>";
|
|
225
278
|
const header = `Possible duplicate issue${candidates.length > 1 ? "s" : ""} found ` +
|
|
226
279
|
"(by title-keyword overlap):\n" +
|
|
227
280
|
`${lines.join("\n")}\n\n` +
|
|
228
281
|
" If one of these is the same work, comment on it instead of creating a new issue.\n";
|
|
229
282
|
if (mode === "advisory") {
|
|
230
283
|
return (header +
|
|
284
|
+
" If this is a piece of one of them rather than a duplicate, attach it with " +
|
|
285
|
+
`\`issues update <new-id> --parent ${parentRef}\`.\n` +
|
|
231
286
|
" This is advisory only (DEV-5590) — creation is proceeding. Pass " +
|
|
232
287
|
"--allow-duplicate to silence this notice next time.");
|
|
233
288
|
}
|
|
234
289
|
return (header +
|
|
235
|
-
" If this is
|
|
290
|
+
" If this is a piece of one of them rather than a duplicate, re-run with " +
|
|
291
|
+
`--parent ${parentRef} ${DUPLICATE_GATE_OVERRIDE_FLAG} to file it as a sub-issue.\n` +
|
|
292
|
+
` If this is genuinely distinct, re-run with ${DUPLICATE_GATE_OVERRIDE_FLAG} to proceed ` +
|
|
236
293
|
"(and consider --related-to to link the related issue).");
|
|
237
294
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enrichlayer/el-linear",
|
|
3
|
-
"version": "1.41.
|
|
3
|
+
"version": "1.41.1",
|
|
4
4
|
"description": "A pragmatic CLI for Linear.app — deterministic team/label/member resolution, structured issue validation, configurable term enforcement, and a GraphQL escape hatch.",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"types": "dist/main.d.ts",
|