@singi-labs/sifa-sdk 0.19.79 → 0.19.81
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/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/jev/error.cjs +96 -0
- package/dist/jev/error.cjs.map +1 -0
- package/dist/jev/error.d.cts +98 -0
- package/dist/jev/error.d.ts +98 -0
- package/dist/jev/error.js +94 -0
- package/dist/jev/error.js.map +1 -0
- package/dist/jev/issue.cjs +183 -0
- package/dist/jev/issue.cjs.map +1 -0
- package/dist/jev/issue.d.cts +119 -0
- package/dist/jev/issue.d.ts +119 -0
- package/dist/jev/issue.js +166 -0
- package/dist/jev/issue.js.map +1 -0
- package/package.json +21 -1
package/dist/index.cjs
CHANGED
|
@@ -7313,7 +7313,7 @@ function describeSifaRecord(collection, value) {
|
|
|
7313
7313
|
}
|
|
7314
7314
|
|
|
7315
7315
|
// src/index.ts
|
|
7316
|
-
var SIFA_SDK_VERSION = "0.19.
|
|
7316
|
+
var SIFA_SDK_VERSION = "0.19.81";
|
|
7317
7317
|
|
|
7318
7318
|
exports.ACTIVITY_TIERS = ACTIVITY_TIERS;
|
|
7319
7319
|
exports.ACTIVITY_VERBS = ACTIVITY_VERBS;
|
package/dist/index.js
CHANGED
|
@@ -370,7 +370,7 @@ import {
|
|
|
370
370
|
roadmapIssueUrl
|
|
371
371
|
} from "./roadmap/index.js";
|
|
372
372
|
import { SIFA_REPO_GROUPS, describeSifaRecord, repoGroupForCollection } from "./repo/index.js";
|
|
373
|
-
var SIFA_SDK_VERSION = "0.19.
|
|
373
|
+
var SIFA_SDK_VERSION = "0.19.81";
|
|
374
374
|
export {
|
|
375
375
|
ACTIVITY_TIERS,
|
|
376
376
|
ACTIVITY_VERBS,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/jev/error.ts
|
|
4
|
+
var DID_RE = /did:(?:plc|web):[A-Za-z0-9._:%-]+/g;
|
|
5
|
+
var EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
6
|
+
var HANDLE_RE = /@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+/g;
|
|
7
|
+
var PROFILE_PATH_RE = /(\/(?:p|profile)\/)[^/\s?#]+/g;
|
|
8
|
+
var AT_URI_RE = /at:\/\/[^\s/]+/g;
|
|
9
|
+
function scrubErrorText(text) {
|
|
10
|
+
return text.replace(AT_URI_RE, "at://<id>").replace(DID_RE, "<did>").replace(EMAIL_RE, "<email>").replace(HANDLE_RE, "<handle>").replace(PROFILE_PATH_RE, "$1<handle>");
|
|
11
|
+
}
|
|
12
|
+
var ERROR_MAX_FRAMES = 8;
|
|
13
|
+
var ISSUE_CANDIDATE_BODY_MAX_CHARS = 2e3;
|
|
14
|
+
function frameState(frames) {
|
|
15
|
+
const inApp = frames.filter((f) => f.inApp !== false);
|
|
16
|
+
const chosen = (inApp.length > 0 ? inApp : frames).slice(0, ERROR_MAX_FRAMES);
|
|
17
|
+
const out = chosen.map((f) => {
|
|
18
|
+
const s = {};
|
|
19
|
+
if (f.filename) s.filename = scrubErrorText(f.filename);
|
|
20
|
+
if (f.function) s.function = scrubErrorText(f.function);
|
|
21
|
+
if (typeof f.lineNo === "number") s.lineNo = f.lineNo;
|
|
22
|
+
return s;
|
|
23
|
+
});
|
|
24
|
+
return out.length > 0 ? out : void 0;
|
|
25
|
+
}
|
|
26
|
+
function buildErrorReportState(report) {
|
|
27
|
+
const state = { title: scrubErrorText(report.title.trim()) };
|
|
28
|
+
if (report.culprit) state.culprit = scrubErrorText(report.culprit);
|
|
29
|
+
if (report.level) state.level = report.level;
|
|
30
|
+
if (report.type) state.type = scrubErrorText(report.type);
|
|
31
|
+
if (report.value) state.value = scrubErrorText(report.value.trim());
|
|
32
|
+
if (report.release) state.release = report.release;
|
|
33
|
+
const frames = report.frames ? frameState(report.frames) : void 0;
|
|
34
|
+
if (frames) state.frames = frames;
|
|
35
|
+
return state;
|
|
36
|
+
}
|
|
37
|
+
function buildIssueCandidateState(issue) {
|
|
38
|
+
const state = {
|
|
39
|
+
number: issue.number,
|
|
40
|
+
title: scrubErrorText(issue.title.trim())
|
|
41
|
+
};
|
|
42
|
+
const body = issue.body?.trim();
|
|
43
|
+
if (body) state.body = scrubErrorText(body.slice(0, ISSUE_CANDIDATE_BODY_MAX_CHARS));
|
|
44
|
+
return state;
|
|
45
|
+
}
|
|
46
|
+
function buildErrorIssuePairState(report, issue) {
|
|
47
|
+
return { error: buildErrorReportState(report), issue: buildIssueCandidateState(issue) };
|
|
48
|
+
}
|
|
49
|
+
var ERROR_SAME_BUG_NOUL_QUESTION = {
|
|
50
|
+
type: "noul",
|
|
51
|
+
instructions: "The state holds an error report (title, culprit, exception, top stack frames) and an existing GitHub issue. Does the issue describe the same underlying bug as the error report?",
|
|
52
|
+
criteria: {
|
|
53
|
+
true: "Same root cause: the same exception at the same code path, or the issue text names the same failure even if line numbers or messages differ slightly.",
|
|
54
|
+
false: "A different bug: different exception type or code path, a different feature, or an issue that only shares generic words (error, 500, profile) with the report."
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var SAME_BUG_THRESHOLD = 0.85;
|
|
58
|
+
function isLikelySameBug(noul, threshold = SAME_BUG_THRESHOLD) {
|
|
59
|
+
return noul >= threshold;
|
|
60
|
+
}
|
|
61
|
+
function buildSameBugQuestions(candidates) {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const c of candidates) {
|
|
64
|
+
out[`sameBug:${c.number}`] = {
|
|
65
|
+
type: "noul",
|
|
66
|
+
instructions: `${ERROR_SAME_BUG_NOUL_QUESTION.instructions} Judge the candidate issue with number ${c.number} in state.candidates.`,
|
|
67
|
+
...ERROR_SAME_BUG_NOUL_QUESTION.criteria ? { criteria: ERROR_SAME_BUG_NOUL_QUESTION.criteria } : {}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function buildErrorCandidatesState(report, candidates) {
|
|
73
|
+
return {
|
|
74
|
+
error: buildErrorReportState(report),
|
|
75
|
+
candidates: candidates.map(buildIssueCandidateState)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function issueNumberFromSameBugKey(key) {
|
|
79
|
+
const m = /^sameBug:(\d+)$/.exec(key);
|
|
80
|
+
return m ? Number(m[1]) : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
exports.ERROR_MAX_FRAMES = ERROR_MAX_FRAMES;
|
|
84
|
+
exports.ERROR_SAME_BUG_NOUL_QUESTION = ERROR_SAME_BUG_NOUL_QUESTION;
|
|
85
|
+
exports.ISSUE_CANDIDATE_BODY_MAX_CHARS = ISSUE_CANDIDATE_BODY_MAX_CHARS;
|
|
86
|
+
exports.SAME_BUG_THRESHOLD = SAME_BUG_THRESHOLD;
|
|
87
|
+
exports.buildErrorCandidatesState = buildErrorCandidatesState;
|
|
88
|
+
exports.buildErrorIssuePairState = buildErrorIssuePairState;
|
|
89
|
+
exports.buildErrorReportState = buildErrorReportState;
|
|
90
|
+
exports.buildIssueCandidateState = buildIssueCandidateState;
|
|
91
|
+
exports.buildSameBugQuestions = buildSameBugQuestions;
|
|
92
|
+
exports.isLikelySameBug = isLikelySameBug;
|
|
93
|
+
exports.issueNumberFromSameBugKey = issueNumberFromSameBugKey;
|
|
94
|
+
exports.scrubErrorText = scrubErrorText;
|
|
95
|
+
//# sourceMappingURL=error.cjs.map
|
|
96
|
+
//# sourceMappingURL=error.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/jev/error.ts"],"names":[],"mappings":"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","file":"error.cjs","sourcesContent":["/**\n * Error-report judgments for the GlitchTip relay (sifa-workspace decision\n * 2026-10-05, background agents tier policy, Phase 2). Pure builders: the\n * state for \"is this error event the same bug as an existing issue?\", the\n * Noul question, the threshold, and the scrubber that removes identifiers\n * before anything leaves the server. No network, no key.\n *\n * Privacy: GlitchTip frames and messages can embed handles, DIDs, emails and\n * URLs with user paths. {@link scrubErrorText} runs over every string placed\n * in the state; callers must not bypass it.\n */\n\nimport type { NoulQuestion } from './index.js';\n\n// ---------------------------------------------------------------------------\n// Scrubbing\n// ---------------------------------------------------------------------------\n\nconst DID_RE = /did:(?:plc|web):[A-Za-z0-9._:%-]+/g;\nconst EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/g;\n/** `@handle.tld` mentions; a bare `@` inside code (decorators) has no dot-tld. */\nconst HANDLE_RE = /@[A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)+/g;\n/** `/p/<handle>` and `/profile/<handle>` path segments on Sifa URLs. */\nconst PROFILE_PATH_RE = /(\\/(?:p|profile)\\/)[^/\\s?#]+/g;\n/** at:// URIs carry a DID or handle as authority. */\nconst AT_URI_RE = /at:\\/\\/[^\\s/]+/g;\n\n/** Replace identifiers with stable placeholders. Idempotent. */\nexport function scrubErrorText(text: string): string {\n return text\n .replace(AT_URI_RE, 'at://<id>')\n .replace(DID_RE, '<did>')\n .replace(EMAIL_RE, '<email>')\n .replace(HANDLE_RE, '<handle>')\n .replace(PROFILE_PATH_RE, '$1<handle>');\n}\n\n// ---------------------------------------------------------------------------\n// Input and state\n// ---------------------------------------------------------------------------\n\n/** One stack frame, already reduced to what a same-bug judgment needs. */\nexport interface ErrorFrameInput {\n filename?: string;\n function?: string;\n lineNo?: number;\n inApp?: boolean;\n}\n\n/** A GlitchTip issue (error group) as seen by the relay. */\nexport interface ErrorReportInput {\n title: string;\n culprit?: string;\n level?: string;\n /** Exception type, e.g. `TypeError`. */\n type?: string;\n /** Exception value / message. */\n value?: string;\n frames?: ErrorFrameInput[];\n /** Release or deploy identifier, when GlitchTip has it. */\n release?: string;\n}\n\n/** An existing GitHub issue candidate (title + body, labelled `glitchtip`). */\nexport interface IssueCandidateInput {\n number: number;\n title: string;\n body?: string;\n}\n\n/** Keep the judgment cheap and bounded: top in-app frames only. */\nexport const ERROR_MAX_FRAMES = 8;\nexport const ISSUE_CANDIDATE_BODY_MAX_CHARS = 2000;\n\nexport interface ErrorReportState {\n title: string;\n culprit?: string;\n level?: string;\n type?: string;\n value?: string;\n release?: string;\n frames?: Array<{ filename?: string; function?: string; lineNo?: number }>;\n}\n\nexport interface IssueCandidateState {\n number: number;\n title: string;\n body?: string;\n}\n\nfunction frameState(frames: ErrorFrameInput[]): ErrorReportState['frames'] {\n const inApp = frames.filter((f) => f.inApp !== false);\n const chosen = (inApp.length > 0 ? inApp : frames).slice(0, ERROR_MAX_FRAMES);\n const out = chosen.map((f) => {\n const s: { filename?: string; function?: string; lineNo?: number } = {};\n if (f.filename) s.filename = scrubErrorText(f.filename);\n if (f.function) s.function = scrubErrorText(f.function);\n if (typeof f.lineNo === 'number') s.lineNo = f.lineNo;\n return s;\n });\n return out.length > 0 ? out : undefined;\n}\n\n/** Scrubbed, bounded state for the error side of the pair. */\nexport function buildErrorReportState(report: ErrorReportInput): ErrorReportState {\n const state: ErrorReportState = { title: scrubErrorText(report.title.trim()) };\n if (report.culprit) state.culprit = scrubErrorText(report.culprit);\n if (report.level) state.level = report.level;\n if (report.type) state.type = scrubErrorText(report.type);\n if (report.value) state.value = scrubErrorText(report.value.trim());\n if (report.release) state.release = report.release;\n const frames = report.frames ? frameState(report.frames) : undefined;\n if (frames) state.frames = frames;\n return state;\n}\n\n/** Scrubbed, bounded state for the GitHub issue side of the pair. */\nexport function buildIssueCandidateState(issue: IssueCandidateInput): IssueCandidateState {\n const state: IssueCandidateState = {\n number: issue.number,\n title: scrubErrorText(issue.title.trim()),\n };\n const body = issue.body?.trim();\n if (body) state.body = scrubErrorText(body.slice(0, ISSUE_CANDIDATE_BODY_MAX_CHARS));\n return state;\n}\n\n/** State for one error-vs-issue pair, both under stable named fields. */\nexport function buildErrorIssuePairState(\n report: ErrorReportInput,\n issue: IssueCandidateInput,\n): { error: ErrorReportState; issue: IssueCandidateState } {\n return { error: buildErrorReportState(report), issue: buildIssueCandidateState(issue) };\n}\n\n// ---------------------------------------------------------------------------\n// Question and threshold\n// ---------------------------------------------------------------------------\n\n/** Static Noul question paired with {@link buildErrorIssuePairState}. */\nexport const ERROR_SAME_BUG_NOUL_QUESTION: NoulQuestion = {\n type: 'noul',\n instructions:\n 'The state holds an error report (title, culprit, exception, top stack frames) and an existing GitHub issue. Does the issue describe the same underlying bug as the error report?',\n criteria: {\n true: 'Same root cause: the same exception at the same code path, or the issue text names the same failure even if line numbers or messages differ slightly.',\n false:\n 'A different bug: different exception type or code path, a different feature, or an issue that only shares generic words (error, 500, profile) with the report.',\n },\n};\n\n/**\n * Precision over recall: a false \"same bug\" hides a new error behind an old\n * issue; a false \"new\" costs one duplicate issue a human closes. Validate on\n * labelled GlitchTip-event-to-issue pairs before widening.\n */\nexport const SAME_BUG_THRESHOLD = 0.85;\n\n/** True when the Noul probability clears the same-bug threshold. */\nexport function isLikelySameBug(noul: number, threshold: number = SAME_BUG_THRESHOLD): boolean {\n return noul >= threshold;\n}\n\n/**\n * Question ids for a batched call over several candidates: one Noul per\n * candidate, keyed `sameBug:<issueNumber>`. Pair with one state holding\n * `error` plus `candidates[]`; each question names its candidate number.\n */\nexport function buildSameBugQuestions(\n candidates: IssueCandidateInput[],\n): Record<string, NoulQuestion> {\n const out: Record<string, NoulQuestion> = {};\n for (const c of candidates) {\n out[`sameBug:${c.number}`] = {\n type: 'noul',\n instructions: `${ERROR_SAME_BUG_NOUL_QUESTION.instructions} Judge the candidate issue with number ${c.number} in state.candidates.`,\n ...(ERROR_SAME_BUG_NOUL_QUESTION.criteria\n ? { criteria: ERROR_SAME_BUG_NOUL_QUESTION.criteria }\n : {}),\n };\n }\n return out;\n}\n\n/** State for the batched form: one error against several candidates. */\nexport function buildErrorCandidatesState(\n report: ErrorReportInput,\n candidates: IssueCandidateInput[],\n): { error: ErrorReportState; candidates: IssueCandidateState[] } {\n return {\n error: buildErrorReportState(report),\n candidates: candidates.map(buildIssueCandidateState),\n };\n}\n\n/** Parse `sameBug:<n>` back to the issue number, or null for a foreign key. */\nexport function issueNumberFromSameBugKey(key: string): number | null {\n const m = /^sameBug:(\\d+)$/.exec(key);\n return m ? Number(m[1]) : null;\n}\n"]}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { NoulQuestion } from './index.cjs';
|
|
2
|
+
import 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error-report judgments for the GlitchTip relay (sifa-workspace decision
|
|
6
|
+
* 2026-10-05, background agents tier policy, Phase 2). Pure builders: the
|
|
7
|
+
* state for "is this error event the same bug as an existing issue?", the
|
|
8
|
+
* Noul question, the threshold, and the scrubber that removes identifiers
|
|
9
|
+
* before anything leaves the server. No network, no key.
|
|
10
|
+
*
|
|
11
|
+
* Privacy: GlitchTip frames and messages can embed handles, DIDs, emails and
|
|
12
|
+
* URLs with user paths. {@link scrubErrorText} runs over every string placed
|
|
13
|
+
* in the state; callers must not bypass it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Replace identifiers with stable placeholders. Idempotent. */
|
|
17
|
+
declare function scrubErrorText(text: string): string;
|
|
18
|
+
/** One stack frame, already reduced to what a same-bug judgment needs. */
|
|
19
|
+
interface ErrorFrameInput {
|
|
20
|
+
filename?: string;
|
|
21
|
+
function?: string;
|
|
22
|
+
lineNo?: number;
|
|
23
|
+
inApp?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** A GlitchTip issue (error group) as seen by the relay. */
|
|
26
|
+
interface ErrorReportInput {
|
|
27
|
+
title: string;
|
|
28
|
+
culprit?: string;
|
|
29
|
+
level?: string;
|
|
30
|
+
/** Exception type, e.g. `TypeError`. */
|
|
31
|
+
type?: string;
|
|
32
|
+
/** Exception value / message. */
|
|
33
|
+
value?: string;
|
|
34
|
+
frames?: ErrorFrameInput[];
|
|
35
|
+
/** Release or deploy identifier, when GlitchTip has it. */
|
|
36
|
+
release?: string;
|
|
37
|
+
}
|
|
38
|
+
/** An existing GitHub issue candidate (title + body, labelled `glitchtip`). */
|
|
39
|
+
interface IssueCandidateInput {
|
|
40
|
+
number: number;
|
|
41
|
+
title: string;
|
|
42
|
+
body?: string;
|
|
43
|
+
}
|
|
44
|
+
/** Keep the judgment cheap and bounded: top in-app frames only. */
|
|
45
|
+
declare const ERROR_MAX_FRAMES = 8;
|
|
46
|
+
declare const ISSUE_CANDIDATE_BODY_MAX_CHARS = 2000;
|
|
47
|
+
interface ErrorReportState {
|
|
48
|
+
title: string;
|
|
49
|
+
culprit?: string;
|
|
50
|
+
level?: string;
|
|
51
|
+
type?: string;
|
|
52
|
+
value?: string;
|
|
53
|
+
release?: string;
|
|
54
|
+
frames?: Array<{
|
|
55
|
+
filename?: string;
|
|
56
|
+
function?: string;
|
|
57
|
+
lineNo?: number;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
interface IssueCandidateState {
|
|
61
|
+
number: number;
|
|
62
|
+
title: string;
|
|
63
|
+
body?: string;
|
|
64
|
+
}
|
|
65
|
+
/** Scrubbed, bounded state for the error side of the pair. */
|
|
66
|
+
declare function buildErrorReportState(report: ErrorReportInput): ErrorReportState;
|
|
67
|
+
/** Scrubbed, bounded state for the GitHub issue side of the pair. */
|
|
68
|
+
declare function buildIssueCandidateState(issue: IssueCandidateInput): IssueCandidateState;
|
|
69
|
+
/** State for one error-vs-issue pair, both under stable named fields. */
|
|
70
|
+
declare function buildErrorIssuePairState(report: ErrorReportInput, issue: IssueCandidateInput): {
|
|
71
|
+
error: ErrorReportState;
|
|
72
|
+
issue: IssueCandidateState;
|
|
73
|
+
};
|
|
74
|
+
/** Static Noul question paired with {@link buildErrorIssuePairState}. */
|
|
75
|
+
declare const ERROR_SAME_BUG_NOUL_QUESTION: NoulQuestion;
|
|
76
|
+
/**
|
|
77
|
+
* Precision over recall: a false "same bug" hides a new error behind an old
|
|
78
|
+
* issue; a false "new" costs one duplicate issue a human closes. Validate on
|
|
79
|
+
* labelled GlitchTip-event-to-issue pairs before widening.
|
|
80
|
+
*/
|
|
81
|
+
declare const SAME_BUG_THRESHOLD = 0.85;
|
|
82
|
+
/** True when the Noul probability clears the same-bug threshold. */
|
|
83
|
+
declare function isLikelySameBug(noul: number, threshold?: number): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Question ids for a batched call over several candidates: one Noul per
|
|
86
|
+
* candidate, keyed `sameBug:<issueNumber>`. Pair with one state holding
|
|
87
|
+
* `error` plus `candidates[]`; each question names its candidate number.
|
|
88
|
+
*/
|
|
89
|
+
declare function buildSameBugQuestions(candidates: IssueCandidateInput[]): Record<string, NoulQuestion>;
|
|
90
|
+
/** State for the batched form: one error against several candidates. */
|
|
91
|
+
declare function buildErrorCandidatesState(report: ErrorReportInput, candidates: IssueCandidateInput[]): {
|
|
92
|
+
error: ErrorReportState;
|
|
93
|
+
candidates: IssueCandidateState[];
|
|
94
|
+
};
|
|
95
|
+
/** Parse `sameBug:<n>` back to the issue number, or null for a foreign key. */
|
|
96
|
+
declare function issueNumberFromSameBugKey(key: string): number | null;
|
|
97
|
+
|
|
98
|
+
export { ERROR_MAX_FRAMES, ERROR_SAME_BUG_NOUL_QUESTION, type ErrorFrameInput, type ErrorReportInput, type ErrorReportState, ISSUE_CANDIDATE_BODY_MAX_CHARS, type IssueCandidateInput, type IssueCandidateState, SAME_BUG_THRESHOLD, buildErrorCandidatesState, buildErrorIssuePairState, buildErrorReportState, buildIssueCandidateState, buildSameBugQuestions, isLikelySameBug, issueNumberFromSameBugKey, scrubErrorText };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { NoulQuestion } from './index.js';
|
|
2
|
+
import 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error-report judgments for the GlitchTip relay (sifa-workspace decision
|
|
6
|
+
* 2026-10-05, background agents tier policy, Phase 2). Pure builders: the
|
|
7
|
+
* state for "is this error event the same bug as an existing issue?", the
|
|
8
|
+
* Noul question, the threshold, and the scrubber that removes identifiers
|
|
9
|
+
* before anything leaves the server. No network, no key.
|
|
10
|
+
*
|
|
11
|
+
* Privacy: GlitchTip frames and messages can embed handles, DIDs, emails and
|
|
12
|
+
* URLs with user paths. {@link scrubErrorText} runs over every string placed
|
|
13
|
+
* in the state; callers must not bypass it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Replace identifiers with stable placeholders. Idempotent. */
|
|
17
|
+
declare function scrubErrorText(text: string): string;
|
|
18
|
+
/** One stack frame, already reduced to what a same-bug judgment needs. */
|
|
19
|
+
interface ErrorFrameInput {
|
|
20
|
+
filename?: string;
|
|
21
|
+
function?: string;
|
|
22
|
+
lineNo?: number;
|
|
23
|
+
inApp?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** A GlitchTip issue (error group) as seen by the relay. */
|
|
26
|
+
interface ErrorReportInput {
|
|
27
|
+
title: string;
|
|
28
|
+
culprit?: string;
|
|
29
|
+
level?: string;
|
|
30
|
+
/** Exception type, e.g. `TypeError`. */
|
|
31
|
+
type?: string;
|
|
32
|
+
/** Exception value / message. */
|
|
33
|
+
value?: string;
|
|
34
|
+
frames?: ErrorFrameInput[];
|
|
35
|
+
/** Release or deploy identifier, when GlitchTip has it. */
|
|
36
|
+
release?: string;
|
|
37
|
+
}
|
|
38
|
+
/** An existing GitHub issue candidate (title + body, labelled `glitchtip`). */
|
|
39
|
+
interface IssueCandidateInput {
|
|
40
|
+
number: number;
|
|
41
|
+
title: string;
|
|
42
|
+
body?: string;
|
|
43
|
+
}
|
|
44
|
+
/** Keep the judgment cheap and bounded: top in-app frames only. */
|
|
45
|
+
declare const ERROR_MAX_FRAMES = 8;
|
|
46
|
+
declare const ISSUE_CANDIDATE_BODY_MAX_CHARS = 2000;
|
|
47
|
+
interface ErrorReportState {
|
|
48
|
+
title: string;
|
|
49
|
+
culprit?: string;
|
|
50
|
+
level?: string;
|
|
51
|
+
type?: string;
|
|
52
|
+
value?: string;
|
|
53
|
+
release?: string;
|
|
54
|
+
frames?: Array<{
|
|
55
|
+
filename?: string;
|
|
56
|
+
function?: string;
|
|
57
|
+
lineNo?: number;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
interface IssueCandidateState {
|
|
61
|
+
number: number;
|
|
62
|
+
title: string;
|
|
63
|
+
body?: string;
|
|
64
|
+
}
|
|
65
|
+
/** Scrubbed, bounded state for the error side of the pair. */
|
|
66
|
+
declare function buildErrorReportState(report: ErrorReportInput): ErrorReportState;
|
|
67
|
+
/** Scrubbed, bounded state for the GitHub issue side of the pair. */
|
|
68
|
+
declare function buildIssueCandidateState(issue: IssueCandidateInput): IssueCandidateState;
|
|
69
|
+
/** State for one error-vs-issue pair, both under stable named fields. */
|
|
70
|
+
declare function buildErrorIssuePairState(report: ErrorReportInput, issue: IssueCandidateInput): {
|
|
71
|
+
error: ErrorReportState;
|
|
72
|
+
issue: IssueCandidateState;
|
|
73
|
+
};
|
|
74
|
+
/** Static Noul question paired with {@link buildErrorIssuePairState}. */
|
|
75
|
+
declare const ERROR_SAME_BUG_NOUL_QUESTION: NoulQuestion;
|
|
76
|
+
/**
|
|
77
|
+
* Precision over recall: a false "same bug" hides a new error behind an old
|
|
78
|
+
* issue; a false "new" costs one duplicate issue a human closes. Validate on
|
|
79
|
+
* labelled GlitchTip-event-to-issue pairs before widening.
|
|
80
|
+
*/
|
|
81
|
+
declare const SAME_BUG_THRESHOLD = 0.85;
|
|
82
|
+
/** True when the Noul probability clears the same-bug threshold. */
|
|
83
|
+
declare function isLikelySameBug(noul: number, threshold?: number): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Question ids for a batched call over several candidates: one Noul per
|
|
86
|
+
* candidate, keyed `sameBug:<issueNumber>`. Pair with one state holding
|
|
87
|
+
* `error` plus `candidates[]`; each question names its candidate number.
|
|
88
|
+
*/
|
|
89
|
+
declare function buildSameBugQuestions(candidates: IssueCandidateInput[]): Record<string, NoulQuestion>;
|
|
90
|
+
/** State for the batched form: one error against several candidates. */
|
|
91
|
+
declare function buildErrorCandidatesState(report: ErrorReportInput, candidates: IssueCandidateInput[]): {
|
|
92
|
+
error: ErrorReportState;
|
|
93
|
+
candidates: IssueCandidateState[];
|
|
94
|
+
};
|
|
95
|
+
/** Parse `sameBug:<n>` back to the issue number, or null for a foreign key. */
|
|
96
|
+
declare function issueNumberFromSameBugKey(key: string): number | null;
|
|
97
|
+
|
|
98
|
+
export { ERROR_MAX_FRAMES, ERROR_SAME_BUG_NOUL_QUESTION, type ErrorFrameInput, type ErrorReportInput, type ErrorReportState, ISSUE_CANDIDATE_BODY_MAX_CHARS, type IssueCandidateInput, type IssueCandidateState, SAME_BUG_THRESHOLD, buildErrorCandidatesState, buildErrorIssuePairState, buildErrorReportState, buildIssueCandidateState, buildSameBugQuestions, isLikelySameBug, issueNumberFromSameBugKey, scrubErrorText };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// src/jev/error.ts
|
|
2
|
+
var DID_RE = /did:(?:plc|web):[A-Za-z0-9._:%-]+/g;
|
|
3
|
+
var EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
4
|
+
var HANDLE_RE = /@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+/g;
|
|
5
|
+
var PROFILE_PATH_RE = /(\/(?:p|profile)\/)[^/\s?#]+/g;
|
|
6
|
+
var AT_URI_RE = /at:\/\/[^\s/]+/g;
|
|
7
|
+
function scrubErrorText(text) {
|
|
8
|
+
return text.replace(AT_URI_RE, "at://<id>").replace(DID_RE, "<did>").replace(EMAIL_RE, "<email>").replace(HANDLE_RE, "<handle>").replace(PROFILE_PATH_RE, "$1<handle>");
|
|
9
|
+
}
|
|
10
|
+
var ERROR_MAX_FRAMES = 8;
|
|
11
|
+
var ISSUE_CANDIDATE_BODY_MAX_CHARS = 2e3;
|
|
12
|
+
function frameState(frames) {
|
|
13
|
+
const inApp = frames.filter((f) => f.inApp !== false);
|
|
14
|
+
const chosen = (inApp.length > 0 ? inApp : frames).slice(0, ERROR_MAX_FRAMES);
|
|
15
|
+
const out = chosen.map((f) => {
|
|
16
|
+
const s = {};
|
|
17
|
+
if (f.filename) s.filename = scrubErrorText(f.filename);
|
|
18
|
+
if (f.function) s.function = scrubErrorText(f.function);
|
|
19
|
+
if (typeof f.lineNo === "number") s.lineNo = f.lineNo;
|
|
20
|
+
return s;
|
|
21
|
+
});
|
|
22
|
+
return out.length > 0 ? out : void 0;
|
|
23
|
+
}
|
|
24
|
+
function buildErrorReportState(report) {
|
|
25
|
+
const state = { title: scrubErrorText(report.title.trim()) };
|
|
26
|
+
if (report.culprit) state.culprit = scrubErrorText(report.culprit);
|
|
27
|
+
if (report.level) state.level = report.level;
|
|
28
|
+
if (report.type) state.type = scrubErrorText(report.type);
|
|
29
|
+
if (report.value) state.value = scrubErrorText(report.value.trim());
|
|
30
|
+
if (report.release) state.release = report.release;
|
|
31
|
+
const frames = report.frames ? frameState(report.frames) : void 0;
|
|
32
|
+
if (frames) state.frames = frames;
|
|
33
|
+
return state;
|
|
34
|
+
}
|
|
35
|
+
function buildIssueCandidateState(issue) {
|
|
36
|
+
const state = {
|
|
37
|
+
number: issue.number,
|
|
38
|
+
title: scrubErrorText(issue.title.trim())
|
|
39
|
+
};
|
|
40
|
+
const body = issue.body?.trim();
|
|
41
|
+
if (body) state.body = scrubErrorText(body.slice(0, ISSUE_CANDIDATE_BODY_MAX_CHARS));
|
|
42
|
+
return state;
|
|
43
|
+
}
|
|
44
|
+
function buildErrorIssuePairState(report, issue) {
|
|
45
|
+
return { error: buildErrorReportState(report), issue: buildIssueCandidateState(issue) };
|
|
46
|
+
}
|
|
47
|
+
var ERROR_SAME_BUG_NOUL_QUESTION = {
|
|
48
|
+
type: "noul",
|
|
49
|
+
instructions: "The state holds an error report (title, culprit, exception, top stack frames) and an existing GitHub issue. Does the issue describe the same underlying bug as the error report?",
|
|
50
|
+
criteria: {
|
|
51
|
+
true: "Same root cause: the same exception at the same code path, or the issue text names the same failure even if line numbers or messages differ slightly.",
|
|
52
|
+
false: "A different bug: different exception type or code path, a different feature, or an issue that only shares generic words (error, 500, profile) with the report."
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var SAME_BUG_THRESHOLD = 0.85;
|
|
56
|
+
function isLikelySameBug(noul, threshold = SAME_BUG_THRESHOLD) {
|
|
57
|
+
return noul >= threshold;
|
|
58
|
+
}
|
|
59
|
+
function buildSameBugQuestions(candidates) {
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const c of candidates) {
|
|
62
|
+
out[`sameBug:${c.number}`] = {
|
|
63
|
+
type: "noul",
|
|
64
|
+
instructions: `${ERROR_SAME_BUG_NOUL_QUESTION.instructions} Judge the candidate issue with number ${c.number} in state.candidates.`,
|
|
65
|
+
...ERROR_SAME_BUG_NOUL_QUESTION.criteria ? { criteria: ERROR_SAME_BUG_NOUL_QUESTION.criteria } : {}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function buildErrorCandidatesState(report, candidates) {
|
|
71
|
+
return {
|
|
72
|
+
error: buildErrorReportState(report),
|
|
73
|
+
candidates: candidates.map(buildIssueCandidateState)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function issueNumberFromSameBugKey(key) {
|
|
77
|
+
const m = /^sameBug:(\d+)$/.exec(key);
|
|
78
|
+
return m ? Number(m[1]) : null;
|
|
79
|
+
}
|
|
80
|
+
export {
|
|
81
|
+
ERROR_MAX_FRAMES,
|
|
82
|
+
ERROR_SAME_BUG_NOUL_QUESTION,
|
|
83
|
+
ISSUE_CANDIDATE_BODY_MAX_CHARS,
|
|
84
|
+
SAME_BUG_THRESHOLD,
|
|
85
|
+
buildErrorCandidatesState,
|
|
86
|
+
buildErrorIssuePairState,
|
|
87
|
+
buildErrorReportState,
|
|
88
|
+
buildIssueCandidateState,
|
|
89
|
+
buildSameBugQuestions,
|
|
90
|
+
isLikelySameBug,
|
|
91
|
+
issueNumberFromSameBugKey,
|
|
92
|
+
scrubErrorText
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/jev/error.ts"],"sourcesContent":["/**\n * Error-report judgments for the GlitchTip relay (sifa-workspace decision\n * 2026-10-05, background agents tier policy, Phase 2). Pure builders: the\n * state for \"is this error event the same bug as an existing issue?\", the\n * Noul question, the threshold, and the scrubber that removes identifiers\n * before anything leaves the server. No network, no key.\n *\n * Privacy: GlitchTip frames and messages can embed handles, DIDs, emails and\n * URLs with user paths. {@link scrubErrorText} runs over every string placed\n * in the state; callers must not bypass it.\n */\n\nimport type { NoulQuestion } from './index.js';\n\n// ---------------------------------------------------------------------------\n// Scrubbing\n// ---------------------------------------------------------------------------\n\nconst DID_RE = /did:(?:plc|web):[A-Za-z0-9._:%-]+/g;\nconst EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/g;\n/** `@handle.tld` mentions; a bare `@` inside code (decorators) has no dot-tld. */\nconst HANDLE_RE = /@[A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)+/g;\n/** `/p/<handle>` and `/profile/<handle>` path segments on Sifa URLs. */\nconst PROFILE_PATH_RE = /(\\/(?:p|profile)\\/)[^/\\s?#]+/g;\n/** at:// URIs carry a DID or handle as authority. */\nconst AT_URI_RE = /at:\\/\\/[^\\s/]+/g;\n\n/** Replace identifiers with stable placeholders. Idempotent. */\nexport function scrubErrorText(text: string): string {\n return text\n .replace(AT_URI_RE, 'at://<id>')\n .replace(DID_RE, '<did>')\n .replace(EMAIL_RE, '<email>')\n .replace(HANDLE_RE, '<handle>')\n .replace(PROFILE_PATH_RE, '$1<handle>');\n}\n\n// ---------------------------------------------------------------------------\n// Input and state\n// ---------------------------------------------------------------------------\n\n/** One stack frame, already reduced to what a same-bug judgment needs. */\nexport interface ErrorFrameInput {\n filename?: string;\n function?: string;\n lineNo?: number;\n inApp?: boolean;\n}\n\n/** A GlitchTip issue (error group) as seen by the relay. */\nexport interface ErrorReportInput {\n title: string;\n culprit?: string;\n level?: string;\n /** Exception type, e.g. `TypeError`. */\n type?: string;\n /** Exception value / message. */\n value?: string;\n frames?: ErrorFrameInput[];\n /** Release or deploy identifier, when GlitchTip has it. */\n release?: string;\n}\n\n/** An existing GitHub issue candidate (title + body, labelled `glitchtip`). */\nexport interface IssueCandidateInput {\n number: number;\n title: string;\n body?: string;\n}\n\n/** Keep the judgment cheap and bounded: top in-app frames only. */\nexport const ERROR_MAX_FRAMES = 8;\nexport const ISSUE_CANDIDATE_BODY_MAX_CHARS = 2000;\n\nexport interface ErrorReportState {\n title: string;\n culprit?: string;\n level?: string;\n type?: string;\n value?: string;\n release?: string;\n frames?: Array<{ filename?: string; function?: string; lineNo?: number }>;\n}\n\nexport interface IssueCandidateState {\n number: number;\n title: string;\n body?: string;\n}\n\nfunction frameState(frames: ErrorFrameInput[]): ErrorReportState['frames'] {\n const inApp = frames.filter((f) => f.inApp !== false);\n const chosen = (inApp.length > 0 ? inApp : frames).slice(0, ERROR_MAX_FRAMES);\n const out = chosen.map((f) => {\n const s: { filename?: string; function?: string; lineNo?: number } = {};\n if (f.filename) s.filename = scrubErrorText(f.filename);\n if (f.function) s.function = scrubErrorText(f.function);\n if (typeof f.lineNo === 'number') s.lineNo = f.lineNo;\n return s;\n });\n return out.length > 0 ? out : undefined;\n}\n\n/** Scrubbed, bounded state for the error side of the pair. */\nexport function buildErrorReportState(report: ErrorReportInput): ErrorReportState {\n const state: ErrorReportState = { title: scrubErrorText(report.title.trim()) };\n if (report.culprit) state.culprit = scrubErrorText(report.culprit);\n if (report.level) state.level = report.level;\n if (report.type) state.type = scrubErrorText(report.type);\n if (report.value) state.value = scrubErrorText(report.value.trim());\n if (report.release) state.release = report.release;\n const frames = report.frames ? frameState(report.frames) : undefined;\n if (frames) state.frames = frames;\n return state;\n}\n\n/** Scrubbed, bounded state for the GitHub issue side of the pair. */\nexport function buildIssueCandidateState(issue: IssueCandidateInput): IssueCandidateState {\n const state: IssueCandidateState = {\n number: issue.number,\n title: scrubErrorText(issue.title.trim()),\n };\n const body = issue.body?.trim();\n if (body) state.body = scrubErrorText(body.slice(0, ISSUE_CANDIDATE_BODY_MAX_CHARS));\n return state;\n}\n\n/** State for one error-vs-issue pair, both under stable named fields. */\nexport function buildErrorIssuePairState(\n report: ErrorReportInput,\n issue: IssueCandidateInput,\n): { error: ErrorReportState; issue: IssueCandidateState } {\n return { error: buildErrorReportState(report), issue: buildIssueCandidateState(issue) };\n}\n\n// ---------------------------------------------------------------------------\n// Question and threshold\n// ---------------------------------------------------------------------------\n\n/** Static Noul question paired with {@link buildErrorIssuePairState}. */\nexport const ERROR_SAME_BUG_NOUL_QUESTION: NoulQuestion = {\n type: 'noul',\n instructions:\n 'The state holds an error report (title, culprit, exception, top stack frames) and an existing GitHub issue. Does the issue describe the same underlying bug as the error report?',\n criteria: {\n true: 'Same root cause: the same exception at the same code path, or the issue text names the same failure even if line numbers or messages differ slightly.',\n false:\n 'A different bug: different exception type or code path, a different feature, or an issue that only shares generic words (error, 500, profile) with the report.',\n },\n};\n\n/**\n * Precision over recall: a false \"same bug\" hides a new error behind an old\n * issue; a false \"new\" costs one duplicate issue a human closes. Validate on\n * labelled GlitchTip-event-to-issue pairs before widening.\n */\nexport const SAME_BUG_THRESHOLD = 0.85;\n\n/** True when the Noul probability clears the same-bug threshold. */\nexport function isLikelySameBug(noul: number, threshold: number = SAME_BUG_THRESHOLD): boolean {\n return noul >= threshold;\n}\n\n/**\n * Question ids for a batched call over several candidates: one Noul per\n * candidate, keyed `sameBug:<issueNumber>`. Pair with one state holding\n * `error` plus `candidates[]`; each question names its candidate number.\n */\nexport function buildSameBugQuestions(\n candidates: IssueCandidateInput[],\n): Record<string, NoulQuestion> {\n const out: Record<string, NoulQuestion> = {};\n for (const c of candidates) {\n out[`sameBug:${c.number}`] = {\n type: 'noul',\n instructions: `${ERROR_SAME_BUG_NOUL_QUESTION.instructions} Judge the candidate issue with number ${c.number} in state.candidates.`,\n ...(ERROR_SAME_BUG_NOUL_QUESTION.criteria\n ? { criteria: ERROR_SAME_BUG_NOUL_QUESTION.criteria }\n : {}),\n };\n }\n return out;\n}\n\n/** State for the batched form: one error against several candidates. */\nexport function buildErrorCandidatesState(\n report: ErrorReportInput,\n candidates: IssueCandidateInput[],\n): { error: ErrorReportState; candidates: IssueCandidateState[] } {\n return {\n error: buildErrorReportState(report),\n candidates: candidates.map(buildIssueCandidateState),\n };\n}\n\n/** Parse `sameBug:<n>` back to the issue number, or null for a foreign key. */\nexport function issueNumberFromSameBugKey(key: string): number | null {\n const m = /^sameBug:(\\d+)$/.exec(key);\n return m ? Number(m[1]) : null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
|
|
5
|
+
// src/jev/issue.ts
|
|
6
|
+
var noulAnswerSchema = zod.z.object({
|
|
7
|
+
type: zod.z.literal("noul"),
|
|
8
|
+
noul: zod.z.number().min(0).max(1)
|
|
9
|
+
});
|
|
10
|
+
var choiceAnswerSchema = zod.z.object({
|
|
11
|
+
type: zod.z.literal("choice"),
|
|
12
|
+
choice: zod.z.string(),
|
|
13
|
+
confidence: zod.z.number().min(0).max(1),
|
|
14
|
+
probabilities: zod.z.record(zod.z.string(), zod.z.number())
|
|
15
|
+
});
|
|
16
|
+
var FIRMOGRAPHIC_CONFIDENCE_FLOOR = 0.6;
|
|
17
|
+
function pickChoiceAboveFloor(answer, opts = {}) {
|
|
18
|
+
const floor = opts.floor ?? FIRMOGRAPHIC_CONFIDENCE_FLOOR;
|
|
19
|
+
if (answer.confidence < floor) return null;
|
|
20
|
+
if (opts.noMatch !== void 0 && answer.choice === opts.noMatch) return null;
|
|
21
|
+
return answer.choice;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/jev/issue.ts
|
|
25
|
+
var ISSUE_BODY_MAX_CHARS = 6e3;
|
|
26
|
+
function buildIssueTriageState(issue) {
|
|
27
|
+
const state = { repo: issue.repo, title: issue.title.trim() };
|
|
28
|
+
const body = issue.body?.trim();
|
|
29
|
+
if (body) {
|
|
30
|
+
if (body.length > ISSUE_BODY_MAX_CHARS) {
|
|
31
|
+
state.body = body.slice(0, ISSUE_BODY_MAX_CHARS);
|
|
32
|
+
state.bodyTruncated = true;
|
|
33
|
+
} else {
|
|
34
|
+
state.body = body;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (issue.existingLabels && issue.existingLabels.length > 0) {
|
|
38
|
+
state.existingLabels = issue.existingLabels;
|
|
39
|
+
}
|
|
40
|
+
return state;
|
|
41
|
+
}
|
|
42
|
+
var ISSUE_TYPE_TAXONOMY = {
|
|
43
|
+
bug: "Something that exists does not behave as documented or expected (error, crash, wrong output)",
|
|
44
|
+
feature: "A request for new behaviour or capability that does not exist yet",
|
|
45
|
+
question: "A question about how something works, with nothing to build or fix",
|
|
46
|
+
documentation: "Docs, README, comments, or examples are missing, wrong, or unclear",
|
|
47
|
+
"tech-debt": "Internal refactor, cleanup, dependency, or tooling work with no user-visible change",
|
|
48
|
+
spam: "Spam, gibberish, advertising, or a test submission",
|
|
49
|
+
unclear: "Not enough information to decide the type"
|
|
50
|
+
};
|
|
51
|
+
var ISSUE_STREAM_TAXONOMY = {
|
|
52
|
+
sifa: "The Sifa profile, activity stream, ingestion, search, or web app",
|
|
53
|
+
kootana: "Kootana: events, meetings, in-person connect, QR scan, organizer tools",
|
|
54
|
+
unclear: "Cannot tell from the issue"
|
|
55
|
+
};
|
|
56
|
+
var ISSUE_STREAM_LABELS = {
|
|
57
|
+
sifa: null,
|
|
58
|
+
kootana: "stream:kootana",
|
|
59
|
+
unclear: null
|
|
60
|
+
};
|
|
61
|
+
function choiceQuestion2(instructions, criteria) {
|
|
62
|
+
return { type: "choice", instructions, criteria };
|
|
63
|
+
}
|
|
64
|
+
var ISSUE_TYPE_CHOICE_QUESTION = choiceQuestion2(
|
|
65
|
+
"The state holds one GitHub issue (repo, title, body, existing labels). Which single type best describes it?",
|
|
66
|
+
ISSUE_TYPE_TAXONOMY
|
|
67
|
+
);
|
|
68
|
+
var ISSUE_STREAM_CHOICE_QUESTION = choiceQuestion2(
|
|
69
|
+
"Which product stream does this issue concern?",
|
|
70
|
+
ISSUE_STREAM_TAXONOMY
|
|
71
|
+
);
|
|
72
|
+
var ISSUE_ACTIONABLE_NOUL_QUESTION = {
|
|
73
|
+
type: "noul",
|
|
74
|
+
instructions: "Does the issue contain enough information for a maintainer to act on it without asking the reporter anything?",
|
|
75
|
+
criteria: {
|
|
76
|
+
true: "For a bug: steps or a trigger, expected vs actual, and an error or location. For a feature: the problem, the desired outcome, and rough scope.",
|
|
77
|
+
false: "Essential details are missing: no way to reproduce, no expected behaviour, vague wish with no outcome, or a bare title."
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var ISSUE_SECURITY_NOUL_QUESTION = {
|
|
81
|
+
type: "noul",
|
|
82
|
+
instructions: "Does the issue describe a security-relevant problem?",
|
|
83
|
+
criteria: {
|
|
84
|
+
true: "A vulnerability, auth or session flaw, data exposure, abuse or spam vector, secrets handling, or a dependency advisory.",
|
|
85
|
+
false: "An ordinary bug, feature, question, or docs issue with no security angle."
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var ISSUE_REGRESSION_NOUL_QUESTION = {
|
|
89
|
+
type: "noul",
|
|
90
|
+
instructions: "Does the issue report that something which previously worked no longer works?",
|
|
91
|
+
criteria: {
|
|
92
|
+
true: "The reporter states or clearly implies the behaviour worked before a release, deploy, or change.",
|
|
93
|
+
false: "A new bug with no prior-working claim, a feature request, or a question."
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var ISSUE_TRIAGE_QUESTION_IDS = {
|
|
97
|
+
type: "issueType",
|
|
98
|
+
stream: "issueStream",
|
|
99
|
+
actionable: "issueActionable",
|
|
100
|
+
security: "issueSecurity",
|
|
101
|
+
regression: "issueRegression"
|
|
102
|
+
};
|
|
103
|
+
function buildIssueTriageQuestions() {
|
|
104
|
+
return {
|
|
105
|
+
[ISSUE_TRIAGE_QUESTION_IDS.type]: ISSUE_TYPE_CHOICE_QUESTION,
|
|
106
|
+
[ISSUE_TRIAGE_QUESTION_IDS.stream]: ISSUE_STREAM_CHOICE_QUESTION,
|
|
107
|
+
[ISSUE_TRIAGE_QUESTION_IDS.actionable]: ISSUE_ACTIONABLE_NOUL_QUESTION,
|
|
108
|
+
[ISSUE_TRIAGE_QUESTION_IDS.security]: ISSUE_SECURITY_NOUL_QUESTION,
|
|
109
|
+
[ISSUE_TRIAGE_QUESTION_IDS.regression]: ISSUE_REGRESSION_NOUL_QUESTION
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
var issueTriageAnswersSchema = zod.z.object({
|
|
113
|
+
[ISSUE_TRIAGE_QUESTION_IDS.type]: choiceAnswerSchema,
|
|
114
|
+
[ISSUE_TRIAGE_QUESTION_IDS.stream]: choiceAnswerSchema,
|
|
115
|
+
[ISSUE_TRIAGE_QUESTION_IDS.actionable]: noulAnswerSchema,
|
|
116
|
+
[ISSUE_TRIAGE_QUESTION_IDS.security]: noulAnswerSchema,
|
|
117
|
+
[ISSUE_TRIAGE_QUESTION_IDS.regression]: noulAnswerSchema
|
|
118
|
+
});
|
|
119
|
+
var ISSUE_TYPE_CONFIDENCE_FLOOR = 0.6;
|
|
120
|
+
var ISSUE_STREAM_CONFIDENCE_FLOOR = 0.7;
|
|
121
|
+
var ISSUE_NEEDS_INFO_MAX_ACTIONABLE = 0.35;
|
|
122
|
+
var ISSUE_ROUTING_THRESHOLD = 0.85;
|
|
123
|
+
function clearsNoul(answer, threshold) {
|
|
124
|
+
return answer.noul >= threshold;
|
|
125
|
+
}
|
|
126
|
+
function pickType(answer, floor) {
|
|
127
|
+
const choice = pickChoiceAboveFloor(answer, { floor, noMatch: "unclear" });
|
|
128
|
+
return choice !== null && choice in ISSUE_TYPE_TAXONOMY ? choice : null;
|
|
129
|
+
}
|
|
130
|
+
function pickStream(answer, floor) {
|
|
131
|
+
const choice = pickChoiceAboveFloor(answer, { floor, noMatch: "unclear" });
|
|
132
|
+
return choice !== null && choice in ISSUE_STREAM_TAXONOMY ? choice : null;
|
|
133
|
+
}
|
|
134
|
+
function issueLabelsFromAnswers(answers, opts = {}) {
|
|
135
|
+
const typeFloor = opts.typeFloor ?? ISSUE_TYPE_CONFIDENCE_FLOOR;
|
|
136
|
+
const streamFloor = opts.streamFloor ?? ISSUE_STREAM_CONFIDENCE_FLOOR;
|
|
137
|
+
const needsInfoMax = opts.needsInfoMaxActionable ?? ISSUE_NEEDS_INFO_MAX_ACTIONABLE;
|
|
138
|
+
const routing = opts.routingThreshold ?? ISSUE_ROUTING_THRESHOLD;
|
|
139
|
+
const type = pickType(answers[ISSUE_TRIAGE_QUESTION_IDS.type], typeFloor);
|
|
140
|
+
if (type === "spam") {
|
|
141
|
+
return {
|
|
142
|
+
labels: ["spam"],
|
|
143
|
+
type,
|
|
144
|
+
stream: null,
|
|
145
|
+
needsInfo: false,
|
|
146
|
+
security: false,
|
|
147
|
+
regression: false
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const stream = pickStream(answers[ISSUE_TRIAGE_QUESTION_IDS.stream], streamFloor);
|
|
151
|
+
const needsInfo = answers[ISSUE_TRIAGE_QUESTION_IDS.actionable].noul < needsInfoMax;
|
|
152
|
+
const security = clearsNoul(answers[ISSUE_TRIAGE_QUESTION_IDS.security], routing);
|
|
153
|
+
const regression = clearsNoul(answers[ISSUE_TRIAGE_QUESTION_IDS.regression], routing);
|
|
154
|
+
const labels = [];
|
|
155
|
+
if (type !== null) labels.push(type);
|
|
156
|
+
if (security) labels.push("security");
|
|
157
|
+
if (regression) labels.push("regression");
|
|
158
|
+
const streamLabel = stream !== null ? ISSUE_STREAM_LABELS[stream] : null;
|
|
159
|
+
if (streamLabel) labels.push(streamLabel);
|
|
160
|
+
if (needsInfo) labels.push("needs-info");
|
|
161
|
+
return { labels, type, stream, needsInfo, security, regression };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
exports.ISSUE_ACTIONABLE_NOUL_QUESTION = ISSUE_ACTIONABLE_NOUL_QUESTION;
|
|
165
|
+
exports.ISSUE_BODY_MAX_CHARS = ISSUE_BODY_MAX_CHARS;
|
|
166
|
+
exports.ISSUE_NEEDS_INFO_MAX_ACTIONABLE = ISSUE_NEEDS_INFO_MAX_ACTIONABLE;
|
|
167
|
+
exports.ISSUE_REGRESSION_NOUL_QUESTION = ISSUE_REGRESSION_NOUL_QUESTION;
|
|
168
|
+
exports.ISSUE_ROUTING_THRESHOLD = ISSUE_ROUTING_THRESHOLD;
|
|
169
|
+
exports.ISSUE_SECURITY_NOUL_QUESTION = ISSUE_SECURITY_NOUL_QUESTION;
|
|
170
|
+
exports.ISSUE_STREAM_CHOICE_QUESTION = ISSUE_STREAM_CHOICE_QUESTION;
|
|
171
|
+
exports.ISSUE_STREAM_CONFIDENCE_FLOOR = ISSUE_STREAM_CONFIDENCE_FLOOR;
|
|
172
|
+
exports.ISSUE_STREAM_LABELS = ISSUE_STREAM_LABELS;
|
|
173
|
+
exports.ISSUE_STREAM_TAXONOMY = ISSUE_STREAM_TAXONOMY;
|
|
174
|
+
exports.ISSUE_TRIAGE_QUESTION_IDS = ISSUE_TRIAGE_QUESTION_IDS;
|
|
175
|
+
exports.ISSUE_TYPE_CHOICE_QUESTION = ISSUE_TYPE_CHOICE_QUESTION;
|
|
176
|
+
exports.ISSUE_TYPE_CONFIDENCE_FLOOR = ISSUE_TYPE_CONFIDENCE_FLOOR;
|
|
177
|
+
exports.ISSUE_TYPE_TAXONOMY = ISSUE_TYPE_TAXONOMY;
|
|
178
|
+
exports.buildIssueTriageQuestions = buildIssueTriageQuestions;
|
|
179
|
+
exports.buildIssueTriageState = buildIssueTriageState;
|
|
180
|
+
exports.issueLabelsFromAnswers = issueLabelsFromAnswers;
|
|
181
|
+
exports.issueTriageAnswersSchema = issueTriageAnswersSchema;
|
|
182
|
+
//# sourceMappingURL=issue.cjs.map
|
|
183
|
+
//# sourceMappingURL=issue.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/jev/index.ts","../../src/jev/issue.ts"],"names":["z","choiceQuestion"],"mappings":"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","file":"issue.cjs","sourcesContent":["/**\n * `@singi-labs/sifa-sdk/jev` -- TypeSafe System One (Jev) question builders,\n * taxonomies, result schemas, and threshold predicates for Sifa's organisation\n * intelligence.\n *\n * This subpath is pure: it defines the typed *questions* to ask Jev, validates\n * the *answers*, and holds the policy constants (thresholds, taxonomies) that\n * turn a raw judgment into a decision. It performs no network calls and needs\n * no API key, so it is safe to import anywhere. The actual `systemOne` call\n * lives server-side in sifa-api, which passes an API key that must never reach\n * the browser.\n *\n * Judgment shapes:\n * - Duplicate-org detection: a single Noul over a candidate pair.\n * - Firmographic classification: independent Choice questions (industry, type,\n * size) asked together over one org's state.\n *\n * See docs.typesafe.ai for the wire contract. Typed output guarantees the\n * interface, not truth: validate thresholds on labelled Sifa data before\n * enabling any write.\n */\n\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// Wire question types\n// ---------------------------------------------------------------------------\n\n/** A yes/no judgment. `criteria` clarifies what true vs false means. */\nexport interface NoulQuestion {\n type: 'noul';\n instructions: string;\n criteria?: { true: string; false: string };\n}\n\n/** A single-selection judgment. `criteria` maps each option to its description. */\nexport interface ChoiceQuestion {\n type: 'choice';\n instructions: string;\n criteria: Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// Answer schemas (validate Jev's response before code trusts it)\n// ---------------------------------------------------------------------------\n\n/** Jev's Noul answer: `noul` is the probability that the answer is yes (0..1). */\nexport const noulAnswerSchema = z.object({\n type: z.literal('noul'),\n noul: z.number().min(0).max(1),\n});\nexport type NoulAnswer = z.infer<typeof noulAnswerSchema>;\n\n/**\n * Jev's Choice answer: `choice` is the highest-probability option, `confidence`\n * summarises how concentrated the distribution is, `probabilities` sums to 1.\n */\nexport const choiceAnswerSchema = z.object({\n type: z.literal('choice'),\n choice: z.string(),\n confidence: z.number().min(0).max(1),\n probabilities: z.record(z.string(), z.number()),\n});\nexport type ChoiceAnswer = z.infer<typeof choiceAnswerSchema>;\n\n// ---------------------------------------------------------------------------\n// Duplicate-org detection (Noul) -- ticket #560\n// ---------------------------------------------------------------------------\n\n/** Public, non-user fields we compare when judging whether two orgs are the same. */\nexport interface OrgRecordInput {\n name: string;\n domain?: string;\n location?: string;\n description?: string;\n}\n\n/** Build a plain object with only the defined keys (Jev sees no `undefined`). */\nfunction compact<T extends object>(obj: T): Partial<T> {\n const out: Partial<T> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (v !== undefined && v !== null && v !== '') out[k as keyof T] = v as T[keyof T];\n }\n return out;\n}\n\n/**\n * State for the org-pair judgment. Both records live under stable named fields\n * so the question can reference them unambiguously.\n */\nexport function buildOrgPairState(\n a: OrgRecordInput,\n b: OrgRecordInput,\n): { orgA: Partial<OrgRecordInput>; orgB: Partial<OrgRecordInput> } {\n return { orgA: compact(a), orgB: compact(b) };\n}\n\n/** Static Noul question paired with {@link buildOrgPairState}. */\nexport const ORG_PAIR_NOUL_QUESTION: NoulQuestion = {\n type: 'noul',\n instructions:\n 'The state holds two organisation records, orgA and orgB. Do they refer to the same real-world organisation?',\n criteria: {\n true: 'Same organisation, e.g. a legal-suffix or abbreviation variant of the same name, or the same primary domain.',\n false:\n 'Distinct organisations, e.g. a shared word in the name but different domains, locations, or lines of business.',\n },\n};\n\n/**\n * Threshold favouring precision: a wrong merge is worse than a missed one.\n * Validate on labelled pairs (ticket #560) before enabling merges in prod.\n */\nexport const SAME_ORG_THRESHOLD = 0.85;\n\n/** True when the Noul probability clears the merge threshold. */\nexport function isLikelySameOrg(noul: number, threshold: number = SAME_ORG_THRESHOLD): boolean {\n return noul >= threshold;\n}\n\n// ---------------------------------------------------------------------------\n// Firmographic classification (Choice) -- ticket #561\n// ---------------------------------------------------------------------------\n\n/** Industry taxonomy. Single source of truth; `other` is the no-match option. */\nexport const INDUSTRY_TAXONOMY: Record<string, string> = {\n technology: 'Software, IT services, internet, hardware, telecoms',\n finance: 'Banking, insurance, investment, fintech, accounting',\n healthcare: 'Medical care, pharma, biotech, health services',\n education: 'Schools, universities, training, edtech',\n manufacturing: 'Industrial production, hardware, materials, automotive',\n retail: 'Retail, ecommerce, consumer goods, hospitality, food',\n media: 'Media, publishing, entertainment, marketing, advertising',\n energy: 'Energy, utilities, oil and gas, renewables',\n construction: 'Construction, real estate, architecture, engineering',\n transport: 'Transport, logistics, shipping, aviation',\n professional_services: 'Legal, consulting, HR, business services',\n public_sector: 'Government, defence, public administration',\n nonprofit: 'Charities, NGOs, foundations, associations',\n agriculture: 'Agriculture, farming, fishing, forestry',\n other: 'None of the above, or not enough information to decide',\n};\n\n/**\n * Organisation type (the org's FORM, not its sector -- a hospital's type is\n * healthcare_provider while its industry is healthcare). `other` is the no-match\n * option. Prefer the registry `legalForm` where it already encodes the form.\n */\nexport const ORG_TYPE_TAXONOMY: Record<string, string> = {\n company: 'For-profit company or corporation',\n cooperative: 'Member-owned co-operative (e.g. eG, SCOP, mutual, credit union)',\n nonprofit: 'Nonprofit, charity, or NGO',\n foundation: 'Grant-making or endowed foundation (e.g. Stiftung, fonds)',\n government: 'Government body, agency, or public-sector authority',\n educational: 'School, college, or university',\n research_institute: 'Independent research institute or laboratory',\n healthcare_provider: 'Hospital, clinic, or healthcare provider',\n professional_association: 'Professional association, trade body, or trade union',\n agency: 'Agency, consultancy, or professional-services firm',\n religious: 'Religious organisation',\n political: 'Political party or organisation',\n self_employed: 'Sole proprietor, freelancer, or self-employed individual',\n other: 'None of the above, or not enough information to decide',\n};\n\n/** Ordered employee-size bands; `unknown` is the no-match option. */\nexport const SIZE_BAND_TAXONOMY: Record<string, string> = {\n solo: '1 person',\n small: '2 to 10 people',\n medium: '11 to 50 people',\n large: '51 to 250 people',\n enterprise: 'More than 250 people',\n unknown: 'Not enough information to decide',\n};\n\nfunction choiceQuestion(instructions: string, criteria: Record<string, string>): ChoiceQuestion {\n return { type: 'choice', instructions, criteria };\n}\n\nexport const INDUSTRY_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which single industry best describes this organisation?',\n INDUSTRY_TAXONOMY,\n);\n\nexport const ORG_TYPE_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which single organisation type (its form, not its industry sector) best describes this organisation?',\n ORG_TYPE_TAXONOMY,\n);\n\nexport const SIZE_BAND_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which employee-size band best fits this organisation?',\n SIZE_BAND_TAXONOMY,\n);\n\n/**\n * Input for firmographic classification. `enrichmentText` (the org's description)\n * is the strongest evidence, but it is optional: many registry orgs have only a\n * name, domain, country, or link set. Passing those lets Jev classify from the\n * signal we hold rather than skipping the org for want of a description. A bare\n * name alone rarely suffices; a name plus a domain or country often does.\n */\nexport interface FirmographicInput {\n name: string;\n domain?: string;\n country?: string;\n /** Featured/external link labels or hosts (e.g. 'linkedin', 'wikipedia'). */\n links?: string[];\n /** An already-known industry, when set (helps disambiguate type/size). */\n industry?: string;\n enrichmentText?: string;\n}\n\n/**\n * State for the firmographic questions. Drops absent/empty fields (including\n * empty arrays) so Jev never sees a null or `[]` it would have to reason about.\n */\nexport function buildFirmographicState(org: FirmographicInput): Partial<FirmographicInput> {\n const compacted = compact(org);\n if (compacted.links && compacted.links.length === 0) delete compacted.links;\n return compacted;\n}\n\n/**\n * Below this confidence a firmographic field is left unset rather than guessed.\n * Validate per field on labelled orgs (ticket #561).\n */\nexport const FIRMOGRAPHIC_CONFIDENCE_FLOOR = 0.6;\n\n/**\n * Select the classified option, or `null` to leave the field unset. Returns\n * null when confidence is below the floor, or when the selected option is the\n * taxonomy's no-match option.\n */\nexport function pickChoiceAboveFloor(\n answer: ChoiceAnswer,\n opts: { floor?: number; noMatch?: string } = {},\n): string | null {\n const floor = opts.floor ?? FIRMOGRAPHIC_CONFIDENCE_FLOOR;\n if (answer.confidence < floor) return null;\n if (opts.noMatch !== undefined && answer.choice === opts.noMatch) return null;\n return answer.choice;\n}\n","/**\n * Issue-triage judgments for the background-agent chores (sifa-workspace\n * decision 2026-10-05, background agents tier policy). Pure builders: the\n * typed questions to ask Jev about a GitHub issue, the label taxonomies, and\n * the thresholds that turn answers into label suggestions. No network, no key.\n *\n * Scope is deliberately T0 (read-only): the output is a list of label\n * suggestions a workflow may apply from an allowlist, never a code change.\n * Jev only sees public issue text. Exact signals (GlitchTip links, template\n * fields present) stay in code, not in a judgment.\n */\n\nimport { z } from 'zod';\n\nimport {\n choiceAnswerSchema,\n noulAnswerSchema,\n pickChoiceAboveFloor,\n type ChoiceAnswer,\n type ChoiceQuestion,\n type NoulAnswer,\n type NoulQuestion,\n} from './index.js';\n\n// ---------------------------------------------------------------------------\n// Input and state\n// ---------------------------------------------------------------------------\n\n/** Public, non-user fields of a GitHub issue that the judgment may see. */\nexport interface IssueInput {\n repo: string;\n title: string;\n body?: string;\n /** Labels already on the issue (e.g. set by a template). */\n existingLabels?: string[];\n}\n\n/** Longest issue body forwarded to Jev; keeps state bounded and cheap. */\nexport const ISSUE_BODY_MAX_CHARS = 6000;\n\nexport interface IssueTriageState {\n repo: string;\n title: string;\n body?: string;\n bodyTruncated?: true;\n existingLabels?: string[];\n}\n\n/** State for the issue questions. Absent and blank fields are dropped. */\nexport function buildIssueTriageState(issue: IssueInput): IssueTriageState {\n const state: IssueTriageState = { repo: issue.repo, title: issue.title.trim() };\n const body = issue.body?.trim();\n if (body) {\n if (body.length > ISSUE_BODY_MAX_CHARS) {\n state.body = body.slice(0, ISSUE_BODY_MAX_CHARS);\n state.bodyTruncated = true;\n } else {\n state.body = body;\n }\n }\n if (issue.existingLabels && issue.existingLabels.length > 0) {\n state.existingLabels = issue.existingLabels;\n }\n return state;\n}\n\n// ---------------------------------------------------------------------------\n// Taxonomies (keys double as label names where a label exists)\n// ---------------------------------------------------------------------------\n\n/** One type label per issue. `unclear` is the no-match option and never a label. */\nexport const ISSUE_TYPE_TAXONOMY: Record<string, string> = {\n bug: 'Something that exists does not behave as documented or expected (error, crash, wrong output)',\n feature: 'A request for new behaviour or capability that does not exist yet',\n question: 'A question about how something works, with nothing to build or fix',\n documentation: 'Docs, README, comments, or examples are missing, wrong, or unclear',\n 'tech-debt':\n 'Internal refactor, cleanup, dependency, or tooling work with no user-visible change',\n spam: 'Spam, gibberish, advertising, or a test submission',\n unclear: 'Not enough information to decide the type',\n};\n\n/** Which product stream an issue belongs to. `unclear` is the no-match option. */\nexport const ISSUE_STREAM_TAXONOMY: Record<string, string> = {\n sifa: 'The Sifa profile, activity stream, ingestion, search, or web app',\n kootana: 'Kootana: events, meetings, in-person connect, QR scan, organizer tools',\n unclear: 'Cannot tell from the issue',\n};\n\n/** Label applied for the Kootana stream; the Sifa stream carries no label. */\nexport const ISSUE_STREAM_LABELS: Readonly<Record<string, string | null>> = {\n sifa: null,\n kootana: 'stream:kootana',\n unclear: null,\n};\n\n// ---------------------------------------------------------------------------\n// Questions (asked together over one issue state)\n// ---------------------------------------------------------------------------\n\nfunction choiceQuestion(instructions: string, criteria: Record<string, string>): ChoiceQuestion {\n return { type: 'choice', instructions, criteria };\n}\n\nexport const ISSUE_TYPE_CHOICE_QUESTION = choiceQuestion(\n 'The state holds one GitHub issue (repo, title, body, existing labels). Which single type best describes it?',\n ISSUE_TYPE_TAXONOMY,\n);\n\nexport const ISSUE_STREAM_CHOICE_QUESTION = choiceQuestion(\n 'Which product stream does this issue concern?',\n ISSUE_STREAM_TAXONOMY,\n);\n\nexport const ISSUE_ACTIONABLE_NOUL_QUESTION: NoulQuestion = {\n type: 'noul',\n instructions:\n 'Does the issue contain enough information for a maintainer to act on it without asking the reporter anything?',\n criteria: {\n true: 'For a bug: steps or a trigger, expected vs actual, and an error or location. For a feature: the problem, the desired outcome, and rough scope.',\n false:\n 'Essential details are missing: no way to reproduce, no expected behaviour, vague wish with no outcome, or a bare title.',\n },\n};\n\nexport const ISSUE_SECURITY_NOUL_QUESTION: NoulQuestion = {\n type: 'noul',\n instructions: 'Does the issue describe a security-relevant problem?',\n criteria: {\n true: 'A vulnerability, auth or session flaw, data exposure, abuse or spam vector, secrets handling, or a dependency advisory.',\n false: 'An ordinary bug, feature, question, or docs issue with no security angle.',\n },\n};\n\nexport const ISSUE_REGRESSION_NOUL_QUESTION: NoulQuestion = {\n type: 'noul',\n instructions: 'Does the issue report that something which previously worked no longer works?',\n criteria: {\n true: 'The reporter states or clearly implies the behaviour worked before a release, deploy, or change.',\n false: 'A new bug with no prior-working claim, a feature request, or a question.',\n },\n};\n\n/** Stable question ids; the answer keys Jev returns. */\nexport const ISSUE_TRIAGE_QUESTION_IDS = {\n type: 'issueType',\n stream: 'issueStream',\n actionable: 'issueActionable',\n security: 'issueSecurity',\n regression: 'issueRegression',\n} as const;\n\n/** All issue questions, keyed by {@link ISSUE_TRIAGE_QUESTION_IDS}, for one batched call. */\nexport function buildIssueTriageQuestions(): Record<string, ChoiceQuestion | NoulQuestion> {\n return {\n [ISSUE_TRIAGE_QUESTION_IDS.type]: ISSUE_TYPE_CHOICE_QUESTION,\n [ISSUE_TRIAGE_QUESTION_IDS.stream]: ISSUE_STREAM_CHOICE_QUESTION,\n [ISSUE_TRIAGE_QUESTION_IDS.actionable]: ISSUE_ACTIONABLE_NOUL_QUESTION,\n [ISSUE_TRIAGE_QUESTION_IDS.security]: ISSUE_SECURITY_NOUL_QUESTION,\n [ISSUE_TRIAGE_QUESTION_IDS.regression]: ISSUE_REGRESSION_NOUL_QUESTION,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Answers -> label suggestions\n// ---------------------------------------------------------------------------\n\n/** Parsed answers for the batched issue call. */\nexport const issueTriageAnswersSchema = z.object({\n [ISSUE_TRIAGE_QUESTION_IDS.type]: choiceAnswerSchema,\n [ISSUE_TRIAGE_QUESTION_IDS.stream]: choiceAnswerSchema,\n [ISSUE_TRIAGE_QUESTION_IDS.actionable]: noulAnswerSchema,\n [ISSUE_TRIAGE_QUESTION_IDS.security]: noulAnswerSchema,\n [ISSUE_TRIAGE_QUESTION_IDS.regression]: noulAnswerSchema,\n});\nexport type IssueTriageAnswers = z.infer<typeof issueTriageAnswersSchema>;\n\n/**\n * Thresholds. Precision over recall: a wrong label costs a maintainer a click\n * and trains distrust; a missing label costs nothing. Validate on labelled\n * closed issues (sifa-api#1555) before widening.\n */\nexport const ISSUE_TYPE_CONFIDENCE_FLOOR = 0.6;\nexport const ISSUE_STREAM_CONFIDENCE_FLOOR = 0.7;\n/** Below this P(actionable) the issue gets `needs-info`. */\nexport const ISSUE_NEEDS_INFO_MAX_ACTIONABLE = 0.35;\n/** Routing labels (security, regression) need a confident yes. */\nexport const ISSUE_ROUTING_THRESHOLD = 0.85;\n\nexport interface IssueLabelSuggestions {\n /** Labels to apply, in a stable order, each guaranteed to be a real label name. */\n labels: string[];\n /** The type choice after the floor, or null when unset. */\n type: string | null;\n /** The stream choice after the floor, or null when unset. */\n stream: string | null;\n needsInfo: boolean;\n security: boolean;\n regression: boolean;\n}\n\nexport interface IssueLabelOptions {\n typeFloor?: number;\n streamFloor?: number;\n needsInfoMaxActionable?: number;\n routingThreshold?: number;\n}\n\nfunction clearsNoul(answer: NoulAnswer, threshold: number): boolean {\n return answer.noul >= threshold;\n}\n\nfunction pickType(answer: ChoiceAnswer, floor: number): string | null {\n const choice = pickChoiceAboveFloor(answer, { floor, noMatch: 'unclear' });\n return choice !== null && choice in ISSUE_TYPE_TAXONOMY ? choice : null;\n}\n\nfunction pickStream(answer: ChoiceAnswer, floor: number): string | null {\n const choice = pickChoiceAboveFloor(answer, { floor, noMatch: 'unclear' });\n return choice !== null && choice in ISSUE_STREAM_TAXONOMY ? choice : null;\n}\n\n/**\n * Turn validated answers into label suggestions. Spam short-circuits: a spam\n * issue gets only `spam`, no routing or needs-info. A choice outside the\n * taxonomy (Jev invented a key) is treated as unset.\n */\nexport function issueLabelsFromAnswers(\n answers: IssueTriageAnswers,\n opts: IssueLabelOptions = {},\n): IssueLabelSuggestions {\n const typeFloor = opts.typeFloor ?? ISSUE_TYPE_CONFIDENCE_FLOOR;\n const streamFloor = opts.streamFloor ?? ISSUE_STREAM_CONFIDENCE_FLOOR;\n const needsInfoMax = opts.needsInfoMaxActionable ?? ISSUE_NEEDS_INFO_MAX_ACTIONABLE;\n const routing = opts.routingThreshold ?? ISSUE_ROUTING_THRESHOLD;\n\n const type = pickType(answers[ISSUE_TRIAGE_QUESTION_IDS.type], typeFloor);\n if (type === 'spam') {\n return {\n labels: ['spam'],\n type,\n stream: null,\n needsInfo: false,\n security: false,\n regression: false,\n };\n }\n\n const stream = pickStream(answers[ISSUE_TRIAGE_QUESTION_IDS.stream], streamFloor);\n const needsInfo = answers[ISSUE_TRIAGE_QUESTION_IDS.actionable].noul < needsInfoMax;\n const security = clearsNoul(answers[ISSUE_TRIAGE_QUESTION_IDS.security], routing);\n const regression = clearsNoul(answers[ISSUE_TRIAGE_QUESTION_IDS.regression], routing);\n\n const labels: string[] = [];\n if (type !== null) labels.push(type);\n if (security) labels.push('security');\n if (regression) labels.push('regression');\n const streamLabel = stream !== null ? ISSUE_STREAM_LABELS[stream] : null;\n if (streamLabel) labels.push(streamLabel);\n if (needsInfo) labels.push('needs-info');\n\n return { labels, type, stream, needsInfo, security, regression };\n}\n"]}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { NoulQuestion, ChoiceQuestion } from './index.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Issue-triage judgments for the background-agent chores (sifa-workspace
|
|
6
|
+
* decision 2026-10-05, background agents tier policy). Pure builders: the
|
|
7
|
+
* typed questions to ask Jev about a GitHub issue, the label taxonomies, and
|
|
8
|
+
* the thresholds that turn answers into label suggestions. No network, no key.
|
|
9
|
+
*
|
|
10
|
+
* Scope is deliberately T0 (read-only): the output is a list of label
|
|
11
|
+
* suggestions a workflow may apply from an allowlist, never a code change.
|
|
12
|
+
* Jev only sees public issue text. Exact signals (GlitchTip links, template
|
|
13
|
+
* fields present) stay in code, not in a judgment.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Public, non-user fields of a GitHub issue that the judgment may see. */
|
|
17
|
+
interface IssueInput {
|
|
18
|
+
repo: string;
|
|
19
|
+
title: string;
|
|
20
|
+
body?: string;
|
|
21
|
+
/** Labels already on the issue (e.g. set by a template). */
|
|
22
|
+
existingLabels?: string[];
|
|
23
|
+
}
|
|
24
|
+
/** Longest issue body forwarded to Jev; keeps state bounded and cheap. */
|
|
25
|
+
declare const ISSUE_BODY_MAX_CHARS = 6000;
|
|
26
|
+
interface IssueTriageState {
|
|
27
|
+
repo: string;
|
|
28
|
+
title: string;
|
|
29
|
+
body?: string;
|
|
30
|
+
bodyTruncated?: true;
|
|
31
|
+
existingLabels?: string[];
|
|
32
|
+
}
|
|
33
|
+
/** State for the issue questions. Absent and blank fields are dropped. */
|
|
34
|
+
declare function buildIssueTriageState(issue: IssueInput): IssueTriageState;
|
|
35
|
+
/** One type label per issue. `unclear` is the no-match option and never a label. */
|
|
36
|
+
declare const ISSUE_TYPE_TAXONOMY: Record<string, string>;
|
|
37
|
+
/** Which product stream an issue belongs to. `unclear` is the no-match option. */
|
|
38
|
+
declare const ISSUE_STREAM_TAXONOMY: Record<string, string>;
|
|
39
|
+
/** Label applied for the Kootana stream; the Sifa stream carries no label. */
|
|
40
|
+
declare const ISSUE_STREAM_LABELS: Readonly<Record<string, string | null>>;
|
|
41
|
+
declare const ISSUE_TYPE_CHOICE_QUESTION: ChoiceQuestion;
|
|
42
|
+
declare const ISSUE_STREAM_CHOICE_QUESTION: ChoiceQuestion;
|
|
43
|
+
declare const ISSUE_ACTIONABLE_NOUL_QUESTION: NoulQuestion;
|
|
44
|
+
declare const ISSUE_SECURITY_NOUL_QUESTION: NoulQuestion;
|
|
45
|
+
declare const ISSUE_REGRESSION_NOUL_QUESTION: NoulQuestion;
|
|
46
|
+
/** Stable question ids; the answer keys Jev returns. */
|
|
47
|
+
declare const ISSUE_TRIAGE_QUESTION_IDS: {
|
|
48
|
+
readonly type: "issueType";
|
|
49
|
+
readonly stream: "issueStream";
|
|
50
|
+
readonly actionable: "issueActionable";
|
|
51
|
+
readonly security: "issueSecurity";
|
|
52
|
+
readonly regression: "issueRegression";
|
|
53
|
+
};
|
|
54
|
+
/** All issue questions, keyed by {@link ISSUE_TRIAGE_QUESTION_IDS}, for one batched call. */
|
|
55
|
+
declare function buildIssueTriageQuestions(): Record<string, ChoiceQuestion | NoulQuestion>;
|
|
56
|
+
/** Parsed answers for the batched issue call. */
|
|
57
|
+
declare const issueTriageAnswersSchema: z.ZodObject<{
|
|
58
|
+
issueType: z.ZodObject<{
|
|
59
|
+
type: z.ZodLiteral<"choice">;
|
|
60
|
+
choice: z.ZodString;
|
|
61
|
+
confidence: z.ZodNumber;
|
|
62
|
+
probabilities: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
issueStream: z.ZodObject<{
|
|
65
|
+
type: z.ZodLiteral<"choice">;
|
|
66
|
+
choice: z.ZodString;
|
|
67
|
+
confidence: z.ZodNumber;
|
|
68
|
+
probabilities: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
69
|
+
}, z.core.$strip>;
|
|
70
|
+
issueActionable: z.ZodObject<{
|
|
71
|
+
type: z.ZodLiteral<"noul">;
|
|
72
|
+
noul: z.ZodNumber;
|
|
73
|
+
}, z.core.$strip>;
|
|
74
|
+
issueSecurity: z.ZodObject<{
|
|
75
|
+
type: z.ZodLiteral<"noul">;
|
|
76
|
+
noul: z.ZodNumber;
|
|
77
|
+
}, z.core.$strip>;
|
|
78
|
+
issueRegression: z.ZodObject<{
|
|
79
|
+
type: z.ZodLiteral<"noul">;
|
|
80
|
+
noul: z.ZodNumber;
|
|
81
|
+
}, z.core.$strip>;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
type IssueTriageAnswers = z.infer<typeof issueTriageAnswersSchema>;
|
|
84
|
+
/**
|
|
85
|
+
* Thresholds. Precision over recall: a wrong label costs a maintainer a click
|
|
86
|
+
* and trains distrust; a missing label costs nothing. Validate on labelled
|
|
87
|
+
* closed issues (sifa-api#1555) before widening.
|
|
88
|
+
*/
|
|
89
|
+
declare const ISSUE_TYPE_CONFIDENCE_FLOOR = 0.6;
|
|
90
|
+
declare const ISSUE_STREAM_CONFIDENCE_FLOOR = 0.7;
|
|
91
|
+
/** Below this P(actionable) the issue gets `needs-info`. */
|
|
92
|
+
declare const ISSUE_NEEDS_INFO_MAX_ACTIONABLE = 0.35;
|
|
93
|
+
/** Routing labels (security, regression) need a confident yes. */
|
|
94
|
+
declare const ISSUE_ROUTING_THRESHOLD = 0.85;
|
|
95
|
+
interface IssueLabelSuggestions {
|
|
96
|
+
/** Labels to apply, in a stable order, each guaranteed to be a real label name. */
|
|
97
|
+
labels: string[];
|
|
98
|
+
/** The type choice after the floor, or null when unset. */
|
|
99
|
+
type: string | null;
|
|
100
|
+
/** The stream choice after the floor, or null when unset. */
|
|
101
|
+
stream: string | null;
|
|
102
|
+
needsInfo: boolean;
|
|
103
|
+
security: boolean;
|
|
104
|
+
regression: boolean;
|
|
105
|
+
}
|
|
106
|
+
interface IssueLabelOptions {
|
|
107
|
+
typeFloor?: number;
|
|
108
|
+
streamFloor?: number;
|
|
109
|
+
needsInfoMaxActionable?: number;
|
|
110
|
+
routingThreshold?: number;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Turn validated answers into label suggestions. Spam short-circuits: a spam
|
|
114
|
+
* issue gets only `spam`, no routing or needs-info. A choice outside the
|
|
115
|
+
* taxonomy (Jev invented a key) is treated as unset.
|
|
116
|
+
*/
|
|
117
|
+
declare function issueLabelsFromAnswers(answers: IssueTriageAnswers, opts?: IssueLabelOptions): IssueLabelSuggestions;
|
|
118
|
+
|
|
119
|
+
export { ISSUE_ACTIONABLE_NOUL_QUESTION, ISSUE_BODY_MAX_CHARS, ISSUE_NEEDS_INFO_MAX_ACTIONABLE, ISSUE_REGRESSION_NOUL_QUESTION, ISSUE_ROUTING_THRESHOLD, ISSUE_SECURITY_NOUL_QUESTION, ISSUE_STREAM_CHOICE_QUESTION, ISSUE_STREAM_CONFIDENCE_FLOOR, ISSUE_STREAM_LABELS, ISSUE_STREAM_TAXONOMY, ISSUE_TRIAGE_QUESTION_IDS, ISSUE_TYPE_CHOICE_QUESTION, ISSUE_TYPE_CONFIDENCE_FLOOR, ISSUE_TYPE_TAXONOMY, type IssueInput, type IssueLabelOptions, type IssueLabelSuggestions, type IssueTriageAnswers, type IssueTriageState, buildIssueTriageQuestions, buildIssueTriageState, issueLabelsFromAnswers, issueTriageAnswersSchema };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { NoulQuestion, ChoiceQuestion } from './index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Issue-triage judgments for the background-agent chores (sifa-workspace
|
|
6
|
+
* decision 2026-10-05, background agents tier policy). Pure builders: the
|
|
7
|
+
* typed questions to ask Jev about a GitHub issue, the label taxonomies, and
|
|
8
|
+
* the thresholds that turn answers into label suggestions. No network, no key.
|
|
9
|
+
*
|
|
10
|
+
* Scope is deliberately T0 (read-only): the output is a list of label
|
|
11
|
+
* suggestions a workflow may apply from an allowlist, never a code change.
|
|
12
|
+
* Jev only sees public issue text. Exact signals (GlitchTip links, template
|
|
13
|
+
* fields present) stay in code, not in a judgment.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Public, non-user fields of a GitHub issue that the judgment may see. */
|
|
17
|
+
interface IssueInput {
|
|
18
|
+
repo: string;
|
|
19
|
+
title: string;
|
|
20
|
+
body?: string;
|
|
21
|
+
/** Labels already on the issue (e.g. set by a template). */
|
|
22
|
+
existingLabels?: string[];
|
|
23
|
+
}
|
|
24
|
+
/** Longest issue body forwarded to Jev; keeps state bounded and cheap. */
|
|
25
|
+
declare const ISSUE_BODY_MAX_CHARS = 6000;
|
|
26
|
+
interface IssueTriageState {
|
|
27
|
+
repo: string;
|
|
28
|
+
title: string;
|
|
29
|
+
body?: string;
|
|
30
|
+
bodyTruncated?: true;
|
|
31
|
+
existingLabels?: string[];
|
|
32
|
+
}
|
|
33
|
+
/** State for the issue questions. Absent and blank fields are dropped. */
|
|
34
|
+
declare function buildIssueTriageState(issue: IssueInput): IssueTriageState;
|
|
35
|
+
/** One type label per issue. `unclear` is the no-match option and never a label. */
|
|
36
|
+
declare const ISSUE_TYPE_TAXONOMY: Record<string, string>;
|
|
37
|
+
/** Which product stream an issue belongs to. `unclear` is the no-match option. */
|
|
38
|
+
declare const ISSUE_STREAM_TAXONOMY: Record<string, string>;
|
|
39
|
+
/** Label applied for the Kootana stream; the Sifa stream carries no label. */
|
|
40
|
+
declare const ISSUE_STREAM_LABELS: Readonly<Record<string, string | null>>;
|
|
41
|
+
declare const ISSUE_TYPE_CHOICE_QUESTION: ChoiceQuestion;
|
|
42
|
+
declare const ISSUE_STREAM_CHOICE_QUESTION: ChoiceQuestion;
|
|
43
|
+
declare const ISSUE_ACTIONABLE_NOUL_QUESTION: NoulQuestion;
|
|
44
|
+
declare const ISSUE_SECURITY_NOUL_QUESTION: NoulQuestion;
|
|
45
|
+
declare const ISSUE_REGRESSION_NOUL_QUESTION: NoulQuestion;
|
|
46
|
+
/** Stable question ids; the answer keys Jev returns. */
|
|
47
|
+
declare const ISSUE_TRIAGE_QUESTION_IDS: {
|
|
48
|
+
readonly type: "issueType";
|
|
49
|
+
readonly stream: "issueStream";
|
|
50
|
+
readonly actionable: "issueActionable";
|
|
51
|
+
readonly security: "issueSecurity";
|
|
52
|
+
readonly regression: "issueRegression";
|
|
53
|
+
};
|
|
54
|
+
/** All issue questions, keyed by {@link ISSUE_TRIAGE_QUESTION_IDS}, for one batched call. */
|
|
55
|
+
declare function buildIssueTriageQuestions(): Record<string, ChoiceQuestion | NoulQuestion>;
|
|
56
|
+
/** Parsed answers for the batched issue call. */
|
|
57
|
+
declare const issueTriageAnswersSchema: z.ZodObject<{
|
|
58
|
+
issueType: z.ZodObject<{
|
|
59
|
+
type: z.ZodLiteral<"choice">;
|
|
60
|
+
choice: z.ZodString;
|
|
61
|
+
confidence: z.ZodNumber;
|
|
62
|
+
probabilities: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
issueStream: z.ZodObject<{
|
|
65
|
+
type: z.ZodLiteral<"choice">;
|
|
66
|
+
choice: z.ZodString;
|
|
67
|
+
confidence: z.ZodNumber;
|
|
68
|
+
probabilities: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
69
|
+
}, z.core.$strip>;
|
|
70
|
+
issueActionable: z.ZodObject<{
|
|
71
|
+
type: z.ZodLiteral<"noul">;
|
|
72
|
+
noul: z.ZodNumber;
|
|
73
|
+
}, z.core.$strip>;
|
|
74
|
+
issueSecurity: z.ZodObject<{
|
|
75
|
+
type: z.ZodLiteral<"noul">;
|
|
76
|
+
noul: z.ZodNumber;
|
|
77
|
+
}, z.core.$strip>;
|
|
78
|
+
issueRegression: z.ZodObject<{
|
|
79
|
+
type: z.ZodLiteral<"noul">;
|
|
80
|
+
noul: z.ZodNumber;
|
|
81
|
+
}, z.core.$strip>;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
type IssueTriageAnswers = z.infer<typeof issueTriageAnswersSchema>;
|
|
84
|
+
/**
|
|
85
|
+
* Thresholds. Precision over recall: a wrong label costs a maintainer a click
|
|
86
|
+
* and trains distrust; a missing label costs nothing. Validate on labelled
|
|
87
|
+
* closed issues (sifa-api#1555) before widening.
|
|
88
|
+
*/
|
|
89
|
+
declare const ISSUE_TYPE_CONFIDENCE_FLOOR = 0.6;
|
|
90
|
+
declare const ISSUE_STREAM_CONFIDENCE_FLOOR = 0.7;
|
|
91
|
+
/** Below this P(actionable) the issue gets `needs-info`. */
|
|
92
|
+
declare const ISSUE_NEEDS_INFO_MAX_ACTIONABLE = 0.35;
|
|
93
|
+
/** Routing labels (security, regression) need a confident yes. */
|
|
94
|
+
declare const ISSUE_ROUTING_THRESHOLD = 0.85;
|
|
95
|
+
interface IssueLabelSuggestions {
|
|
96
|
+
/** Labels to apply, in a stable order, each guaranteed to be a real label name. */
|
|
97
|
+
labels: string[];
|
|
98
|
+
/** The type choice after the floor, or null when unset. */
|
|
99
|
+
type: string | null;
|
|
100
|
+
/** The stream choice after the floor, or null when unset. */
|
|
101
|
+
stream: string | null;
|
|
102
|
+
needsInfo: boolean;
|
|
103
|
+
security: boolean;
|
|
104
|
+
regression: boolean;
|
|
105
|
+
}
|
|
106
|
+
interface IssueLabelOptions {
|
|
107
|
+
typeFloor?: number;
|
|
108
|
+
streamFloor?: number;
|
|
109
|
+
needsInfoMaxActionable?: number;
|
|
110
|
+
routingThreshold?: number;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Turn validated answers into label suggestions. Spam short-circuits: a spam
|
|
114
|
+
* issue gets only `spam`, no routing or needs-info. A choice outside the
|
|
115
|
+
* taxonomy (Jev invented a key) is treated as unset.
|
|
116
|
+
*/
|
|
117
|
+
declare function issueLabelsFromAnswers(answers: IssueTriageAnswers, opts?: IssueLabelOptions): IssueLabelSuggestions;
|
|
118
|
+
|
|
119
|
+
export { ISSUE_ACTIONABLE_NOUL_QUESTION, ISSUE_BODY_MAX_CHARS, ISSUE_NEEDS_INFO_MAX_ACTIONABLE, ISSUE_REGRESSION_NOUL_QUESTION, ISSUE_ROUTING_THRESHOLD, ISSUE_SECURITY_NOUL_QUESTION, ISSUE_STREAM_CHOICE_QUESTION, ISSUE_STREAM_CONFIDENCE_FLOOR, ISSUE_STREAM_LABELS, ISSUE_STREAM_TAXONOMY, ISSUE_TRIAGE_QUESTION_IDS, ISSUE_TYPE_CHOICE_QUESTION, ISSUE_TYPE_CONFIDENCE_FLOOR, ISSUE_TYPE_TAXONOMY, type IssueInput, type IssueLabelOptions, type IssueLabelSuggestions, type IssueTriageAnswers, type IssueTriageState, buildIssueTriageQuestions, buildIssueTriageState, issueLabelsFromAnswers, issueTriageAnswersSchema };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// src/jev/issue.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
choiceAnswerSchema,
|
|
5
|
+
noulAnswerSchema,
|
|
6
|
+
pickChoiceAboveFloor
|
|
7
|
+
} from "./index.js";
|
|
8
|
+
var ISSUE_BODY_MAX_CHARS = 6e3;
|
|
9
|
+
function buildIssueTriageState(issue) {
|
|
10
|
+
const state = { repo: issue.repo, title: issue.title.trim() };
|
|
11
|
+
const body = issue.body?.trim();
|
|
12
|
+
if (body) {
|
|
13
|
+
if (body.length > ISSUE_BODY_MAX_CHARS) {
|
|
14
|
+
state.body = body.slice(0, ISSUE_BODY_MAX_CHARS);
|
|
15
|
+
state.bodyTruncated = true;
|
|
16
|
+
} else {
|
|
17
|
+
state.body = body;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (issue.existingLabels && issue.existingLabels.length > 0) {
|
|
21
|
+
state.existingLabels = issue.existingLabels;
|
|
22
|
+
}
|
|
23
|
+
return state;
|
|
24
|
+
}
|
|
25
|
+
var ISSUE_TYPE_TAXONOMY = {
|
|
26
|
+
bug: "Something that exists does not behave as documented or expected (error, crash, wrong output)",
|
|
27
|
+
feature: "A request for new behaviour or capability that does not exist yet",
|
|
28
|
+
question: "A question about how something works, with nothing to build or fix",
|
|
29
|
+
documentation: "Docs, README, comments, or examples are missing, wrong, or unclear",
|
|
30
|
+
"tech-debt": "Internal refactor, cleanup, dependency, or tooling work with no user-visible change",
|
|
31
|
+
spam: "Spam, gibberish, advertising, or a test submission",
|
|
32
|
+
unclear: "Not enough information to decide the type"
|
|
33
|
+
};
|
|
34
|
+
var ISSUE_STREAM_TAXONOMY = {
|
|
35
|
+
sifa: "The Sifa profile, activity stream, ingestion, search, or web app",
|
|
36
|
+
kootana: "Kootana: events, meetings, in-person connect, QR scan, organizer tools",
|
|
37
|
+
unclear: "Cannot tell from the issue"
|
|
38
|
+
};
|
|
39
|
+
var ISSUE_STREAM_LABELS = {
|
|
40
|
+
sifa: null,
|
|
41
|
+
kootana: "stream:kootana",
|
|
42
|
+
unclear: null
|
|
43
|
+
};
|
|
44
|
+
function choiceQuestion(instructions, criteria) {
|
|
45
|
+
return { type: "choice", instructions, criteria };
|
|
46
|
+
}
|
|
47
|
+
var ISSUE_TYPE_CHOICE_QUESTION = choiceQuestion(
|
|
48
|
+
"The state holds one GitHub issue (repo, title, body, existing labels). Which single type best describes it?",
|
|
49
|
+
ISSUE_TYPE_TAXONOMY
|
|
50
|
+
);
|
|
51
|
+
var ISSUE_STREAM_CHOICE_QUESTION = choiceQuestion(
|
|
52
|
+
"Which product stream does this issue concern?",
|
|
53
|
+
ISSUE_STREAM_TAXONOMY
|
|
54
|
+
);
|
|
55
|
+
var ISSUE_ACTIONABLE_NOUL_QUESTION = {
|
|
56
|
+
type: "noul",
|
|
57
|
+
instructions: "Does the issue contain enough information for a maintainer to act on it without asking the reporter anything?",
|
|
58
|
+
criteria: {
|
|
59
|
+
true: "For a bug: steps or a trigger, expected vs actual, and an error or location. For a feature: the problem, the desired outcome, and rough scope.",
|
|
60
|
+
false: "Essential details are missing: no way to reproduce, no expected behaviour, vague wish with no outcome, or a bare title."
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var ISSUE_SECURITY_NOUL_QUESTION = {
|
|
64
|
+
type: "noul",
|
|
65
|
+
instructions: "Does the issue describe a security-relevant problem?",
|
|
66
|
+
criteria: {
|
|
67
|
+
true: "A vulnerability, auth or session flaw, data exposure, abuse or spam vector, secrets handling, or a dependency advisory.",
|
|
68
|
+
false: "An ordinary bug, feature, question, or docs issue with no security angle."
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var ISSUE_REGRESSION_NOUL_QUESTION = {
|
|
72
|
+
type: "noul",
|
|
73
|
+
instructions: "Does the issue report that something which previously worked no longer works?",
|
|
74
|
+
criteria: {
|
|
75
|
+
true: "The reporter states or clearly implies the behaviour worked before a release, deploy, or change.",
|
|
76
|
+
false: "A new bug with no prior-working claim, a feature request, or a question."
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var ISSUE_TRIAGE_QUESTION_IDS = {
|
|
80
|
+
type: "issueType",
|
|
81
|
+
stream: "issueStream",
|
|
82
|
+
actionable: "issueActionable",
|
|
83
|
+
security: "issueSecurity",
|
|
84
|
+
regression: "issueRegression"
|
|
85
|
+
};
|
|
86
|
+
function buildIssueTriageQuestions() {
|
|
87
|
+
return {
|
|
88
|
+
[ISSUE_TRIAGE_QUESTION_IDS.type]: ISSUE_TYPE_CHOICE_QUESTION,
|
|
89
|
+
[ISSUE_TRIAGE_QUESTION_IDS.stream]: ISSUE_STREAM_CHOICE_QUESTION,
|
|
90
|
+
[ISSUE_TRIAGE_QUESTION_IDS.actionable]: ISSUE_ACTIONABLE_NOUL_QUESTION,
|
|
91
|
+
[ISSUE_TRIAGE_QUESTION_IDS.security]: ISSUE_SECURITY_NOUL_QUESTION,
|
|
92
|
+
[ISSUE_TRIAGE_QUESTION_IDS.regression]: ISSUE_REGRESSION_NOUL_QUESTION
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
var issueTriageAnswersSchema = z.object({
|
|
96
|
+
[ISSUE_TRIAGE_QUESTION_IDS.type]: choiceAnswerSchema,
|
|
97
|
+
[ISSUE_TRIAGE_QUESTION_IDS.stream]: choiceAnswerSchema,
|
|
98
|
+
[ISSUE_TRIAGE_QUESTION_IDS.actionable]: noulAnswerSchema,
|
|
99
|
+
[ISSUE_TRIAGE_QUESTION_IDS.security]: noulAnswerSchema,
|
|
100
|
+
[ISSUE_TRIAGE_QUESTION_IDS.regression]: noulAnswerSchema
|
|
101
|
+
});
|
|
102
|
+
var ISSUE_TYPE_CONFIDENCE_FLOOR = 0.6;
|
|
103
|
+
var ISSUE_STREAM_CONFIDENCE_FLOOR = 0.7;
|
|
104
|
+
var ISSUE_NEEDS_INFO_MAX_ACTIONABLE = 0.35;
|
|
105
|
+
var ISSUE_ROUTING_THRESHOLD = 0.85;
|
|
106
|
+
function clearsNoul(answer, threshold) {
|
|
107
|
+
return answer.noul >= threshold;
|
|
108
|
+
}
|
|
109
|
+
function pickType(answer, floor) {
|
|
110
|
+
const choice = pickChoiceAboveFloor(answer, { floor, noMatch: "unclear" });
|
|
111
|
+
return choice !== null && choice in ISSUE_TYPE_TAXONOMY ? choice : null;
|
|
112
|
+
}
|
|
113
|
+
function pickStream(answer, floor) {
|
|
114
|
+
const choice = pickChoiceAboveFloor(answer, { floor, noMatch: "unclear" });
|
|
115
|
+
return choice !== null && choice in ISSUE_STREAM_TAXONOMY ? choice : null;
|
|
116
|
+
}
|
|
117
|
+
function issueLabelsFromAnswers(answers, opts = {}) {
|
|
118
|
+
const typeFloor = opts.typeFloor ?? ISSUE_TYPE_CONFIDENCE_FLOOR;
|
|
119
|
+
const streamFloor = opts.streamFloor ?? ISSUE_STREAM_CONFIDENCE_FLOOR;
|
|
120
|
+
const needsInfoMax = opts.needsInfoMaxActionable ?? ISSUE_NEEDS_INFO_MAX_ACTIONABLE;
|
|
121
|
+
const routing = opts.routingThreshold ?? ISSUE_ROUTING_THRESHOLD;
|
|
122
|
+
const type = pickType(answers[ISSUE_TRIAGE_QUESTION_IDS.type], typeFloor);
|
|
123
|
+
if (type === "spam") {
|
|
124
|
+
return {
|
|
125
|
+
labels: ["spam"],
|
|
126
|
+
type,
|
|
127
|
+
stream: null,
|
|
128
|
+
needsInfo: false,
|
|
129
|
+
security: false,
|
|
130
|
+
regression: false
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const stream = pickStream(answers[ISSUE_TRIAGE_QUESTION_IDS.stream], streamFloor);
|
|
134
|
+
const needsInfo = answers[ISSUE_TRIAGE_QUESTION_IDS.actionable].noul < needsInfoMax;
|
|
135
|
+
const security = clearsNoul(answers[ISSUE_TRIAGE_QUESTION_IDS.security], routing);
|
|
136
|
+
const regression = clearsNoul(answers[ISSUE_TRIAGE_QUESTION_IDS.regression], routing);
|
|
137
|
+
const labels = [];
|
|
138
|
+
if (type !== null) labels.push(type);
|
|
139
|
+
if (security) labels.push("security");
|
|
140
|
+
if (regression) labels.push("regression");
|
|
141
|
+
const streamLabel = stream !== null ? ISSUE_STREAM_LABELS[stream] : null;
|
|
142
|
+
if (streamLabel) labels.push(streamLabel);
|
|
143
|
+
if (needsInfo) labels.push("needs-info");
|
|
144
|
+
return { labels, type, stream, needsInfo, security, regression };
|
|
145
|
+
}
|
|
146
|
+
export {
|
|
147
|
+
ISSUE_ACTIONABLE_NOUL_QUESTION,
|
|
148
|
+
ISSUE_BODY_MAX_CHARS,
|
|
149
|
+
ISSUE_NEEDS_INFO_MAX_ACTIONABLE,
|
|
150
|
+
ISSUE_REGRESSION_NOUL_QUESTION,
|
|
151
|
+
ISSUE_ROUTING_THRESHOLD,
|
|
152
|
+
ISSUE_SECURITY_NOUL_QUESTION,
|
|
153
|
+
ISSUE_STREAM_CHOICE_QUESTION,
|
|
154
|
+
ISSUE_STREAM_CONFIDENCE_FLOOR,
|
|
155
|
+
ISSUE_STREAM_LABELS,
|
|
156
|
+
ISSUE_STREAM_TAXONOMY,
|
|
157
|
+
ISSUE_TRIAGE_QUESTION_IDS,
|
|
158
|
+
ISSUE_TYPE_CHOICE_QUESTION,
|
|
159
|
+
ISSUE_TYPE_CONFIDENCE_FLOOR,
|
|
160
|
+
ISSUE_TYPE_TAXONOMY,
|
|
161
|
+
buildIssueTriageQuestions,
|
|
162
|
+
buildIssueTriageState,
|
|
163
|
+
issueLabelsFromAnswers,
|
|
164
|
+
issueTriageAnswersSchema
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=issue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/jev/issue.ts"],"sourcesContent":["/**\n * Issue-triage judgments for the background-agent chores (sifa-workspace\n * decision 2026-10-05, background agents tier policy). Pure builders: the\n * typed questions to ask Jev about a GitHub issue, the label taxonomies, and\n * the thresholds that turn answers into label suggestions. No network, no key.\n *\n * Scope is deliberately T0 (read-only): the output is a list of label\n * suggestions a workflow may apply from an allowlist, never a code change.\n * Jev only sees public issue text. Exact signals (GlitchTip links, template\n * fields present) stay in code, not in a judgment.\n */\n\nimport { z } from 'zod';\n\nimport {\n choiceAnswerSchema,\n noulAnswerSchema,\n pickChoiceAboveFloor,\n type ChoiceAnswer,\n type ChoiceQuestion,\n type NoulAnswer,\n type NoulQuestion,\n} from './index.js';\n\n// ---------------------------------------------------------------------------\n// Input and state\n// ---------------------------------------------------------------------------\n\n/** Public, non-user fields of a GitHub issue that the judgment may see. */\nexport interface IssueInput {\n repo: string;\n title: string;\n body?: string;\n /** Labels already on the issue (e.g. set by a template). */\n existingLabels?: string[];\n}\n\n/** Longest issue body forwarded to Jev; keeps state bounded and cheap. */\nexport const ISSUE_BODY_MAX_CHARS = 6000;\n\nexport interface IssueTriageState {\n repo: string;\n title: string;\n body?: string;\n bodyTruncated?: true;\n existingLabels?: string[];\n}\n\n/** State for the issue questions. Absent and blank fields are dropped. */\nexport function buildIssueTriageState(issue: IssueInput): IssueTriageState {\n const state: IssueTriageState = { repo: issue.repo, title: issue.title.trim() };\n const body = issue.body?.trim();\n if (body) {\n if (body.length > ISSUE_BODY_MAX_CHARS) {\n state.body = body.slice(0, ISSUE_BODY_MAX_CHARS);\n state.bodyTruncated = true;\n } else {\n state.body = body;\n }\n }\n if (issue.existingLabels && issue.existingLabels.length > 0) {\n state.existingLabels = issue.existingLabels;\n }\n return state;\n}\n\n// ---------------------------------------------------------------------------\n// Taxonomies (keys double as label names where a label exists)\n// ---------------------------------------------------------------------------\n\n/** One type label per issue. `unclear` is the no-match option and never a label. */\nexport const ISSUE_TYPE_TAXONOMY: Record<string, string> = {\n bug: 'Something that exists does not behave as documented or expected (error, crash, wrong output)',\n feature: 'A request for new behaviour or capability that does not exist yet',\n question: 'A question about how something works, with nothing to build or fix',\n documentation: 'Docs, README, comments, or examples are missing, wrong, or unclear',\n 'tech-debt':\n 'Internal refactor, cleanup, dependency, or tooling work with no user-visible change',\n spam: 'Spam, gibberish, advertising, or a test submission',\n unclear: 'Not enough information to decide the type',\n};\n\n/** Which product stream an issue belongs to. `unclear` is the no-match option. */\nexport const ISSUE_STREAM_TAXONOMY: Record<string, string> = {\n sifa: 'The Sifa profile, activity stream, ingestion, search, or web app',\n kootana: 'Kootana: events, meetings, in-person connect, QR scan, organizer tools',\n unclear: 'Cannot tell from the issue',\n};\n\n/** Label applied for the Kootana stream; the Sifa stream carries no label. */\nexport const ISSUE_STREAM_LABELS: Readonly<Record<string, string | null>> = {\n sifa: null,\n kootana: 'stream:kootana',\n unclear: null,\n};\n\n// ---------------------------------------------------------------------------\n// Questions (asked together over one issue state)\n// ---------------------------------------------------------------------------\n\nfunction choiceQuestion(instructions: string, criteria: Record<string, string>): ChoiceQuestion {\n return { type: 'choice', instructions, criteria };\n}\n\nexport const ISSUE_TYPE_CHOICE_QUESTION = choiceQuestion(\n 'The state holds one GitHub issue (repo, title, body, existing labels). Which single type best describes it?',\n ISSUE_TYPE_TAXONOMY,\n);\n\nexport const ISSUE_STREAM_CHOICE_QUESTION = choiceQuestion(\n 'Which product stream does this issue concern?',\n ISSUE_STREAM_TAXONOMY,\n);\n\nexport const ISSUE_ACTIONABLE_NOUL_QUESTION: NoulQuestion = {\n type: 'noul',\n instructions:\n 'Does the issue contain enough information for a maintainer to act on it without asking the reporter anything?',\n criteria: {\n true: 'For a bug: steps or a trigger, expected vs actual, and an error or location. For a feature: the problem, the desired outcome, and rough scope.',\n false:\n 'Essential details are missing: no way to reproduce, no expected behaviour, vague wish with no outcome, or a bare title.',\n },\n};\n\nexport const ISSUE_SECURITY_NOUL_QUESTION: NoulQuestion = {\n type: 'noul',\n instructions: 'Does the issue describe a security-relevant problem?',\n criteria: {\n true: 'A vulnerability, auth or session flaw, data exposure, abuse or spam vector, secrets handling, or a dependency advisory.',\n false: 'An ordinary bug, feature, question, or docs issue with no security angle.',\n },\n};\n\nexport const ISSUE_REGRESSION_NOUL_QUESTION: NoulQuestion = {\n type: 'noul',\n instructions: 'Does the issue report that something which previously worked no longer works?',\n criteria: {\n true: 'The reporter states or clearly implies the behaviour worked before a release, deploy, or change.',\n false: 'A new bug with no prior-working claim, a feature request, or a question.',\n },\n};\n\n/** Stable question ids; the answer keys Jev returns. */\nexport const ISSUE_TRIAGE_QUESTION_IDS = {\n type: 'issueType',\n stream: 'issueStream',\n actionable: 'issueActionable',\n security: 'issueSecurity',\n regression: 'issueRegression',\n} as const;\n\n/** All issue questions, keyed by {@link ISSUE_TRIAGE_QUESTION_IDS}, for one batched call. */\nexport function buildIssueTriageQuestions(): Record<string, ChoiceQuestion | NoulQuestion> {\n return {\n [ISSUE_TRIAGE_QUESTION_IDS.type]: ISSUE_TYPE_CHOICE_QUESTION,\n [ISSUE_TRIAGE_QUESTION_IDS.stream]: ISSUE_STREAM_CHOICE_QUESTION,\n [ISSUE_TRIAGE_QUESTION_IDS.actionable]: ISSUE_ACTIONABLE_NOUL_QUESTION,\n [ISSUE_TRIAGE_QUESTION_IDS.security]: ISSUE_SECURITY_NOUL_QUESTION,\n [ISSUE_TRIAGE_QUESTION_IDS.regression]: ISSUE_REGRESSION_NOUL_QUESTION,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Answers -> label suggestions\n// ---------------------------------------------------------------------------\n\n/** Parsed answers for the batched issue call. */\nexport const issueTriageAnswersSchema = z.object({\n [ISSUE_TRIAGE_QUESTION_IDS.type]: choiceAnswerSchema,\n [ISSUE_TRIAGE_QUESTION_IDS.stream]: choiceAnswerSchema,\n [ISSUE_TRIAGE_QUESTION_IDS.actionable]: noulAnswerSchema,\n [ISSUE_TRIAGE_QUESTION_IDS.security]: noulAnswerSchema,\n [ISSUE_TRIAGE_QUESTION_IDS.regression]: noulAnswerSchema,\n});\nexport type IssueTriageAnswers = z.infer<typeof issueTriageAnswersSchema>;\n\n/**\n * Thresholds. Precision over recall: a wrong label costs a maintainer a click\n * and trains distrust; a missing label costs nothing. Validate on labelled\n * closed issues (sifa-api#1555) before widening.\n */\nexport const ISSUE_TYPE_CONFIDENCE_FLOOR = 0.6;\nexport const ISSUE_STREAM_CONFIDENCE_FLOOR = 0.7;\n/** Below this P(actionable) the issue gets `needs-info`. */\nexport const ISSUE_NEEDS_INFO_MAX_ACTIONABLE = 0.35;\n/** Routing labels (security, regression) need a confident yes. */\nexport const ISSUE_ROUTING_THRESHOLD = 0.85;\n\nexport interface IssueLabelSuggestions {\n /** Labels to apply, in a stable order, each guaranteed to be a real label name. */\n labels: string[];\n /** The type choice after the floor, or null when unset. */\n type: string | null;\n /** The stream choice after the floor, or null when unset. */\n stream: string | null;\n needsInfo: boolean;\n security: boolean;\n regression: boolean;\n}\n\nexport interface IssueLabelOptions {\n typeFloor?: number;\n streamFloor?: number;\n needsInfoMaxActionable?: number;\n routingThreshold?: number;\n}\n\nfunction clearsNoul(answer: NoulAnswer, threshold: number): boolean {\n return answer.noul >= threshold;\n}\n\nfunction pickType(answer: ChoiceAnswer, floor: number): string | null {\n const choice = pickChoiceAboveFloor(answer, { floor, noMatch: 'unclear' });\n return choice !== null && choice in ISSUE_TYPE_TAXONOMY ? choice : null;\n}\n\nfunction pickStream(answer: ChoiceAnswer, floor: number): string | null {\n const choice = pickChoiceAboveFloor(answer, { floor, noMatch: 'unclear' });\n return choice !== null && choice in ISSUE_STREAM_TAXONOMY ? choice : null;\n}\n\n/**\n * Turn validated answers into label suggestions. Spam short-circuits: a spam\n * issue gets only `spam`, no routing or needs-info. A choice outside the\n * taxonomy (Jev invented a key) is treated as unset.\n */\nexport function issueLabelsFromAnswers(\n answers: IssueTriageAnswers,\n opts: IssueLabelOptions = {},\n): IssueLabelSuggestions {\n const typeFloor = opts.typeFloor ?? ISSUE_TYPE_CONFIDENCE_FLOOR;\n const streamFloor = opts.streamFloor ?? ISSUE_STREAM_CONFIDENCE_FLOOR;\n const needsInfoMax = opts.needsInfoMaxActionable ?? ISSUE_NEEDS_INFO_MAX_ACTIONABLE;\n const routing = opts.routingThreshold ?? ISSUE_ROUTING_THRESHOLD;\n\n const type = pickType(answers[ISSUE_TRIAGE_QUESTION_IDS.type], typeFloor);\n if (type === 'spam') {\n return {\n labels: ['spam'],\n type,\n stream: null,\n needsInfo: false,\n security: false,\n regression: false,\n };\n }\n\n const stream = pickStream(answers[ISSUE_TRIAGE_QUESTION_IDS.stream], streamFloor);\n const needsInfo = answers[ISSUE_TRIAGE_QUESTION_IDS.actionable].noul < needsInfoMax;\n const security = clearsNoul(answers[ISSUE_TRIAGE_QUESTION_IDS.security], routing);\n const regression = clearsNoul(answers[ISSUE_TRIAGE_QUESTION_IDS.regression], routing);\n\n const labels: string[] = [];\n if (type !== null) labels.push(type);\n if (security) labels.push('security');\n if (regression) labels.push('regression');\n const streamLabel = stream !== null ? ISSUE_STREAM_LABELS[stream] : null;\n if (streamLabel) labels.push(streamLabel);\n if (needsInfo) labels.push('needs-info');\n\n return { labels, type, stream, needsInfo, security, regression };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@singi-labs/sifa-sdk",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.81",
|
|
4
4
|
"description": "Sifa SDK — public client library for the Sifa AppView on AT Protocol. Shared by sifa-web and sifa-app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -172,6 +172,26 @@
|
|
|
172
172
|
"default": "./dist/jev/index.cjs"
|
|
173
173
|
}
|
|
174
174
|
},
|
|
175
|
+
"./jev/issue": {
|
|
176
|
+
"import": {
|
|
177
|
+
"types": "./dist/jev/issue.d.ts",
|
|
178
|
+
"default": "./dist/jev/issue.js"
|
|
179
|
+
},
|
|
180
|
+
"require": {
|
|
181
|
+
"types": "./dist/jev/issue.d.cts",
|
|
182
|
+
"default": "./dist/jev/issue.cjs"
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
"./jev/error": {
|
|
186
|
+
"import": {
|
|
187
|
+
"types": "./dist/jev/error.d.ts",
|
|
188
|
+
"default": "./dist/jev/error.js"
|
|
189
|
+
},
|
|
190
|
+
"require": {
|
|
191
|
+
"types": "./dist/jev/error.d.cts",
|
|
192
|
+
"default": "./dist/jev/error.cjs"
|
|
193
|
+
}
|
|
194
|
+
},
|
|
175
195
|
"./rpg": {
|
|
176
196
|
"import": {
|
|
177
197
|
"types": "./dist/rpg/index.d.ts",
|