@yanolja-next/noya-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -0
- package/dist/src/adapters.d.ts +25 -0
- package/dist/src/adapters.js +244 -0
- package/dist/src/issueCompiler.d.ts +17 -0
- package/dist/src/issueCompiler.js +81 -0
- package/dist/src/orchestrator.d.ts +59 -0
- package/dist/src/orchestrator.js +594 -0
- package/dist/src/redaction.d.ts +6 -0
- package/dist/src/redaction.js +53 -0
- package/dist/src/runner.d.ts +26 -0
- package/dist/src/runner.js +423 -0
- package/dist/src/sdk.d.ts +64 -0
- package/dist/src/sdk.js +172 -0
- package/dist/src/sentry.d.ts +7 -0
- package/dist/src/sentry.js +125 -0
- package/dist/src/server.d.ts +10 -0
- package/dist/src/server.js +498 -0
- package/dist/src/store.d.ts +18 -0
- package/dist/src/store.js +58 -0
- package/dist/src/types.d.ts +147 -0
- package/dist/src/types.js +1 -0
- package/package.json +46 -0
- package/public/app.js +185 -0
- package/public/index.html +80 -0
- package/public/styles.css +253 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Noya
|
|
2
|
+
|
|
3
|
+
Production issue reports into pre-triaged Jira tickets and ready-to-review PRs.
|
|
4
|
+
|
|
5
|
+
This repository contains a local Node.js + TypeScript MVP:
|
|
6
|
+
|
|
7
|
+
- Browser SDK wrapper entry point: `src/sdk.ts`
|
|
8
|
+
- Backend orchestrator and dashboard: `src/server.ts`, `public/`
|
|
9
|
+
- Customer runner: `src/runner.ts`
|
|
10
|
+
- Local issue and draft PR artifacts by default
|
|
11
|
+
- Optional GitHub/Jira issue providers when explicitly configured with token
|
|
12
|
+
environment variables
|
|
13
|
+
- GitHub/Jira issues receive a fix-result comment after local draft PR creation
|
|
14
|
+
- Project integration config rejects inline credentials; use `*_env` settings
|
|
15
|
+
for token/secret references
|
|
16
|
+
- Optional telemetry event lookup when a project has provider integration settings
|
|
17
|
+
- Optional provider webhook HMAC verification via
|
|
18
|
+
`integrations.telemetry.webhook_secret_env`
|
|
19
|
+
- SDK session recording wiring with optional Replay/Feedback integration
|
|
20
|
+
registration
|
|
21
|
+
- Optional runner Bearer authentication via project `runner.token_env`
|
|
22
|
+
- Approved fix command refs for local Claude Code/agent execution, with
|
|
23
|
+
shell-free process spawning, isolated repo checkout, and local draft PR
|
|
24
|
+
artifacts
|
|
25
|
+
- Checkout status and patch artifacts are attached to local draft PR output when
|
|
26
|
+
an approved agent changes files
|
|
27
|
+
- Artifact serving is constrained to each project's `artifact_dir`
|
|
28
|
+
- Optional dashboard/admin artifact protection via `NOYA_ADMIN_TOKEN`
|
|
29
|
+
- Project-level artifact retention cleanup via
|
|
30
|
+
`POST /api/projects/:project_key/retention/cleanup`
|
|
31
|
+
- Idempotent manual fix approval so repeated clicks do not create duplicate jobs
|
|
32
|
+
- Audit entries for individual collector policy decisions and fix agent command
|
|
33
|
+
execution
|
|
34
|
+
- SDK network correlation for `x-request-id`, `x-trace-id`, and `traceparent`
|
|
35
|
+
- SDK/provider screenshot URLs are preserved as issue evidence
|
|
36
|
+
- Telemetry breadcrumbs are surfaced in compiled issue bodies
|
|
37
|
+
- Runner lease timeout/requeue and collector `max_entries` / `max_bytes` limits
|
|
38
|
+
- Server-side validation for collect/fix runner completion payloads
|
|
39
|
+
- No-runner flow creates an immediate issue with unavailable internal context
|
|
40
|
+
clearly marked
|
|
41
|
+
- Dashboard links for local issue and draft PR artifacts
|
|
42
|
+
- Dashboard readiness checks for artifact storage, runner repo, tokens, and issue
|
|
43
|
+
providers
|
|
44
|
+
- Dashboard project config editor with server-side validation
|
|
45
|
+
- Project edits preserve immutable `created_at` metadata
|
|
46
|
+
- agency-platform customer sample: `examples/agency-platform/`
|
|
47
|
+
|
|
48
|
+
The local implementation intentionally does not deploy, push, merge, or write to
|
|
49
|
+
production systems. The PR flow creates a local draft PR artifact for review.
|
|
50
|
+
External issue providers are opt-in and are covered by mock-server tests.
|
|
51
|
+
|
|
52
|
+
- [PRD](docs/PRD.md)
|
|
53
|
+
- [AI agent integration guide](docs/AGENT_INTEGRATION.md)
|
|
54
|
+
- [GCP deployment guide](docs/DEPLOY_GCP.md)
|
|
55
|
+
|
|
56
|
+
## Run Locally
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install
|
|
60
|
+
npm run build
|
|
61
|
+
npm start
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Open `http://localhost:3456`.
|
|
65
|
+
|
|
66
|
+
To simulate the customer runner once:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm run runner -- examples/agency-platform/noya.runner.json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If the project is configured with `runner.token_env`, export the same token
|
|
73
|
+
before running the runner:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
export NOYA_AGENCY_PLATFORM_RUNNER_TOKEN=local-runner-token
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
To protect dashboard, local artifact, project config, retention cleanup, and fix
|
|
80
|
+
approval endpoints in a shared local environment:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
export NOYA_ADMIN_TOKEN=local-admin-token
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The dashboard sends this token from `localStorage.noya_admin_token` when set.
|
|
87
|
+
|
|
88
|
+
## Verify
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm test
|
|
92
|
+
npm run test:e2e
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`npm run test:e2e` starts the orchestrator, submits an agency-platform sample
|
|
96
|
+
report, runs collect mode, approves a local fix from the dashboard in headless
|
|
97
|
+
Chrome, runs fix mode, and verifies that a local draft PR artifact was created.
|
|
98
|
+
It also verifies telemetry webhook intake, collector policy denial for raw command
|
|
99
|
+
collectors, telemetry event lookup, GitHub/Jira providers against local mock
|
|
100
|
+
servers, approved fix command execution, runner heartbeat/fail lifecycle, and
|
|
101
|
+
runner Bearer authentication. `npm test` also verifies provider webhook signature
|
|
102
|
+
checks, stale runner lease requeue, collector output limits, duplicate issue
|
|
103
|
+
linking, idempotent fix approval, collector/agent audit entries,
|
|
104
|
+
browser-visible artifact links, readiness checks, SDK request correlation,
|
|
105
|
+
project config validation, context redaction, and recording tag/context wiring.
|
|
106
|
+
It also checks artifact retention cleanup and artifact path boundary enforcement.
|
|
107
|
+
Optional admin-token protection is covered by API tests.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Job, JsonObject, Project, Issue } from "./types.js";
|
|
2
|
+
import type { JsonStore } from "./store.js";
|
|
3
|
+
export declare function createProviderIssue({ store, project, issue, }: {
|
|
4
|
+
store: JsonStore;
|
|
5
|
+
project: Project;
|
|
6
|
+
issue: Issue;
|
|
7
|
+
}): Promise<JsonObject>;
|
|
8
|
+
export declare function updateProviderIssueAfterFix({ store, project, issue, draftPr, result, }: {
|
|
9
|
+
store: JsonStore;
|
|
10
|
+
project: Project;
|
|
11
|
+
issue: Issue;
|
|
12
|
+
draftPr: JsonObject;
|
|
13
|
+
result: JsonObject;
|
|
14
|
+
}): Promise<JsonObject | null>;
|
|
15
|
+
export declare function createLocalDraftPr({ store, project, job, result, }: {
|
|
16
|
+
store: JsonStore;
|
|
17
|
+
project: Project;
|
|
18
|
+
job: Job;
|
|
19
|
+
result: {
|
|
20
|
+
branch?: string;
|
|
21
|
+
changed_files?: string[];
|
|
22
|
+
tests?: string[];
|
|
23
|
+
transcript?: string;
|
|
24
|
+
};
|
|
25
|
+
}): JsonObject;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export async function createProviderIssue({ store, project, issue, }) {
|
|
4
|
+
if (project.issue_provider === "github") {
|
|
5
|
+
return createGitHubIssue({ store, project, issue });
|
|
6
|
+
}
|
|
7
|
+
if (project.issue_provider === "jira") {
|
|
8
|
+
return createJiraIssue({ store, project, issue });
|
|
9
|
+
}
|
|
10
|
+
return createLocalIssue({ store, project, issue });
|
|
11
|
+
}
|
|
12
|
+
export async function updateProviderIssueAfterFix({ store, project, issue, draftPr, result, }) {
|
|
13
|
+
if (project.issue_provider === "github") {
|
|
14
|
+
return createGitHubFixComment({ store, project, issue, draftPr, result });
|
|
15
|
+
}
|
|
16
|
+
if (project.issue_provider === "jira") {
|
|
17
|
+
return createJiraFixComment({ store, project, issue, draftPr, result });
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
function createLocalIssue({ store, project, issue, }) {
|
|
22
|
+
const dir = path.join(project.artifact_dir ?? ".noya/artifacts", "issues");
|
|
23
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
24
|
+
const filePath = path.join(dir, `${issue.id}.md`);
|
|
25
|
+
fs.writeFileSync(filePath, `# ${issue.title}\n\n${issue.body}\n`);
|
|
26
|
+
store.audit("issue.created.local", {
|
|
27
|
+
project_key: project.project_key,
|
|
28
|
+
issue_id: issue.id,
|
|
29
|
+
path: filePath,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
provider: project.issue_provider ?? "local",
|
|
33
|
+
external_id: issue.id,
|
|
34
|
+
url: `file://${filePath}`,
|
|
35
|
+
web_url: `/artifacts/issues/${issue.id}`,
|
|
36
|
+
path: filePath,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function createGitHubIssue({ store, project, issue, }) {
|
|
40
|
+
const github = (project.integrations?.github ?? {});
|
|
41
|
+
const apiBase = String(github.api_base ?? "https://api.github.com");
|
|
42
|
+
const owner = requiredString(github.owner, "github.owner");
|
|
43
|
+
const repo = requiredString(github.repo, "github.repo");
|
|
44
|
+
const token = tokenFromEnv(requiredString(github.token_env, "github.token_env"));
|
|
45
|
+
const response = await fetch(`${apiBase}/repos/${owner}/${repo}/issues`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: {
|
|
48
|
+
authorization: `Bearer ${token}`,
|
|
49
|
+
accept: "application/vnd.github+json",
|
|
50
|
+
"content-type": "application/json",
|
|
51
|
+
"user-agent": "noya-local",
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({ title: issue.title, body: issue.body }),
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok)
|
|
56
|
+
throw new Error(`GitHub issue creation failed: ${response.status}`);
|
|
57
|
+
const payload = (await response.json());
|
|
58
|
+
store.audit("issue.created.github", {
|
|
59
|
+
project_key: project.project_key,
|
|
60
|
+
issue_id: issue.id,
|
|
61
|
+
external_id: String(payload.number ?? payload.id ?? ""),
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
provider: "github",
|
|
65
|
+
external_id: String(payload.number ?? payload.id ?? ""),
|
|
66
|
+
url: String(payload.html_url ?? payload.url ?? ""),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function createJiraIssue({ store, project, issue, }) {
|
|
70
|
+
const jira = (project.integrations?.jira ?? {});
|
|
71
|
+
const apiBase = requiredString(jira.api_base, "jira.api_base").replace(/\/$/, "");
|
|
72
|
+
const projectKey = requiredString(jira.project_key, "jira.project_key");
|
|
73
|
+
const issueType = String(jira.issue_type ?? "Bug");
|
|
74
|
+
const email = requiredString(jira.email, "jira.email");
|
|
75
|
+
const token = tokenFromEnv(requiredString(jira.token_env, "jira.token_env"));
|
|
76
|
+
const response = await fetch(`${apiBase}/rest/api/3/issue`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: {
|
|
79
|
+
authorization: `Basic ${Buffer.from(`${email}:${token}`).toString("base64")}`,
|
|
80
|
+
accept: "application/json",
|
|
81
|
+
"content-type": "application/json",
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
fields: {
|
|
85
|
+
project: { key: projectKey },
|
|
86
|
+
issuetype: { name: issueType },
|
|
87
|
+
summary: issue.title,
|
|
88
|
+
description: jiraDoc(issue.body),
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok)
|
|
93
|
+
throw new Error(`Jira issue creation failed: ${response.status}`);
|
|
94
|
+
const payload = (await response.json());
|
|
95
|
+
store.audit("issue.created.jira", {
|
|
96
|
+
project_key: project.project_key,
|
|
97
|
+
issue_id: issue.id,
|
|
98
|
+
external_id: String(payload.key ?? payload.id ?? ""),
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
provider: "jira",
|
|
102
|
+
external_id: String(payload.key ?? payload.id ?? ""),
|
|
103
|
+
url: payload.self ? String(payload.self) : "",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function createGitHubFixComment({ store, project, issue, draftPr, result, }) {
|
|
107
|
+
const github = (project.integrations?.github ?? {});
|
|
108
|
+
const apiBase = String(github.api_base ?? "https://api.github.com");
|
|
109
|
+
const owner = requiredString(github.owner, "github.owner");
|
|
110
|
+
const repo = requiredString(github.repo, "github.repo");
|
|
111
|
+
const token = tokenFromEnv(requiredString(github.token_env, "github.token_env"));
|
|
112
|
+
const issueNumber = requiredString(issue.integration?.external_id, "github issue external_id");
|
|
113
|
+
const response = await fetch(`${apiBase}/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
authorization: `Bearer ${token}`,
|
|
117
|
+
accept: "application/vnd.github+json",
|
|
118
|
+
"content-type": "application/json",
|
|
119
|
+
"user-agent": "noya-local",
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({ body: fixCommentMarkdown({ draftPr, result }) }),
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok)
|
|
124
|
+
throw new Error(`GitHub issue comment failed: ${response.status}`);
|
|
125
|
+
const payload = (await response.json());
|
|
126
|
+
store.audit("fix.provider_update.github", {
|
|
127
|
+
project_key: project.project_key,
|
|
128
|
+
issue_id: issue.id,
|
|
129
|
+
external_id: issueNumber,
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
provider: "github",
|
|
133
|
+
external_id: String(payload.id ?? ""),
|
|
134
|
+
url: String(payload.html_url ?? payload.url ?? ""),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function createJiraFixComment({ store, project, issue, draftPr, result, }) {
|
|
138
|
+
const jira = (project.integrations?.jira ?? {});
|
|
139
|
+
const apiBase = requiredString(jira.api_base, "jira.api_base").replace(/\/$/, "");
|
|
140
|
+
const email = requiredString(jira.email, "jira.email");
|
|
141
|
+
const token = tokenFromEnv(requiredString(jira.token_env, "jira.token_env"));
|
|
142
|
+
const issueKey = requiredString(issue.integration?.external_id, "jira issue external_id");
|
|
143
|
+
const response = await fetch(`${apiBase}/rest/api/3/issue/${encodeURIComponent(issueKey)}/comment`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: {
|
|
146
|
+
authorization: `Basic ${Buffer.from(`${email}:${token}`).toString("base64")}`,
|
|
147
|
+
accept: "application/json",
|
|
148
|
+
"content-type": "application/json",
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify({ body: jiraDoc(fixCommentMarkdown({ draftPr, result })) }),
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok)
|
|
153
|
+
throw new Error(`Jira issue comment failed: ${response.status}`);
|
|
154
|
+
const payload = (await response.json());
|
|
155
|
+
store.audit("fix.provider_update.jira", {
|
|
156
|
+
project_key: project.project_key,
|
|
157
|
+
issue_id: issue.id,
|
|
158
|
+
external_id: issueKey,
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
provider: "jira",
|
|
162
|
+
external_id: String(payload.id ?? ""),
|
|
163
|
+
url: String(payload.self ?? ""),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function fixCommentMarkdown({ draftPr, result }) {
|
|
167
|
+
return [
|
|
168
|
+
"## Noya Fix Result",
|
|
169
|
+
"",
|
|
170
|
+
`Local draft PR: ${String(draftPr.web_url ?? draftPr.url ?? "unavailable")}`,
|
|
171
|
+
`Branch: ${String(result.branch ?? "unavailable")}`,
|
|
172
|
+
"",
|
|
173
|
+
"### Changed Files",
|
|
174
|
+
bulletList(result.changed_files),
|
|
175
|
+
"",
|
|
176
|
+
"### Verification",
|
|
177
|
+
bulletList(result.tests),
|
|
178
|
+
"",
|
|
179
|
+
"Push, merge, deploy, and production writes were not performed by Noya.",
|
|
180
|
+
].join("\n");
|
|
181
|
+
}
|
|
182
|
+
function bulletList(value) {
|
|
183
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
184
|
+
return "- Unavailable.";
|
|
185
|
+
return value.map((entry) => `- ${String(entry)}`).join("\n");
|
|
186
|
+
}
|
|
187
|
+
function requiredString(value, name) {
|
|
188
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
189
|
+
throw new Error(`Missing ${name} integration setting`);
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
function tokenFromEnv(name) {
|
|
194
|
+
const value = process.env[name];
|
|
195
|
+
if (!value)
|
|
196
|
+
throw new Error(`Missing integration token env var: ${name}`);
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
function jiraDoc(markdown) {
|
|
200
|
+
return {
|
|
201
|
+
type: "doc",
|
|
202
|
+
version: 1,
|
|
203
|
+
content: [
|
|
204
|
+
{
|
|
205
|
+
type: "paragraph",
|
|
206
|
+
content: [{ type: "text", text: markdown.slice(0, 32000) }],
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
export function createLocalDraftPr({ store, project, job, result, }) {
|
|
212
|
+
const dir = path.join(project.artifact_dir ?? ".noya/artifacts", "prs");
|
|
213
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
214
|
+
const filePath = path.join(dir, `${job.id}.md`);
|
|
215
|
+
const body = [
|
|
216
|
+
`# Local Draft PR: ${job.issue_id}`,
|
|
217
|
+
"",
|
|
218
|
+
`Project: ${project.name}`,
|
|
219
|
+
`Job: ${job.id}`,
|
|
220
|
+
`Branch: ${result.branch ?? "noya/local-draft"}`,
|
|
221
|
+
"",
|
|
222
|
+
"## Changed Files",
|
|
223
|
+
...(result.changed_files ?? ["No file edits were made by the dry-run agent."]).map((file) => `- ${file}`),
|
|
224
|
+
"",
|
|
225
|
+
"## Verification",
|
|
226
|
+
...(result.tests ?? ["No tests configured."]).map((test) => `- ${test}`),
|
|
227
|
+
"",
|
|
228
|
+
"## Transcript",
|
|
229
|
+
result.transcript ?? "No transcript.",
|
|
230
|
+
].join("\n");
|
|
231
|
+
fs.writeFileSync(filePath, `${body}\n`);
|
|
232
|
+
store.audit("fix.local_draft_pr.created", {
|
|
233
|
+
project_key: project.project_key,
|
|
234
|
+
job_id: job.id,
|
|
235
|
+
path: filePath,
|
|
236
|
+
});
|
|
237
|
+
return {
|
|
238
|
+
provider: "local_draft_pr",
|
|
239
|
+
external_id: job.id,
|
|
240
|
+
url: `file://${filePath}`,
|
|
241
|
+
web_url: `/artifacts/prs/${job.id}`,
|
|
242
|
+
path: filePath,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { JsonValue, Project, Report } from "./types.js";
|
|
2
|
+
type CollectorResult = {
|
|
3
|
+
context?: {
|
|
4
|
+
logs?: JsonValue;
|
|
5
|
+
domain_snapshot?: JsonValue;
|
|
6
|
+
};
|
|
7
|
+
policy?: JsonValue;
|
|
8
|
+
};
|
|
9
|
+
export declare function compileIssue({ report, project, collectorResult, }: {
|
|
10
|
+
report: Report;
|
|
11
|
+
project: Project;
|
|
12
|
+
collectorResult?: CollectorResult | null;
|
|
13
|
+
}): {
|
|
14
|
+
title: string;
|
|
15
|
+
body: string;
|
|
16
|
+
};
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
function bulletList(values) {
|
|
2
|
+
if (!values || values.length === 0)
|
|
3
|
+
return "- Unavailable.";
|
|
4
|
+
return values.map((value) => `- ${value}`).join("\n");
|
|
5
|
+
}
|
|
6
|
+
function objectBlock(value) {
|
|
7
|
+
if (!value || typeof value !== "object" || Object.keys(value).length === 0)
|
|
8
|
+
return "Unavailable.";
|
|
9
|
+
return `\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
|
|
10
|
+
}
|
|
11
|
+
function optionalStrings(values) {
|
|
12
|
+
return values.filter((value) => typeof value === "string" && value.length > 0);
|
|
13
|
+
}
|
|
14
|
+
function feedbackMessage(feedback) {
|
|
15
|
+
if (!feedback || typeof feedback !== "object" || Array.isArray(feedback))
|
|
16
|
+
return undefined;
|
|
17
|
+
const value = feedback.message;
|
|
18
|
+
return typeof value === "string" ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
export function compileIssue({ report, project, collectorResult, }) {
|
|
21
|
+
const context = collectorResult?.context ?? {};
|
|
22
|
+
const telemetry = report.telemetry ?? report.sentry ?? {};
|
|
23
|
+
const hints = report.hints ?? {};
|
|
24
|
+
const summary = report.message ||
|
|
25
|
+
feedbackMessage(report.feedback) ||
|
|
26
|
+
`${project.name}: production issue report ${report.id}`;
|
|
27
|
+
const title = `[${project.name}] ${summary}`.slice(0, 140);
|
|
28
|
+
const body = [
|
|
29
|
+
`# Summary\n${summary}`,
|
|
30
|
+
`# Impact\n${report.impact || "User-reported production issue. Impact needs owner confirmation."}`,
|
|
31
|
+
`# Environment\n${objectBlock({
|
|
32
|
+
project: project.name,
|
|
33
|
+
environment: report.environment,
|
|
34
|
+
release: report.release,
|
|
35
|
+
route: report.route,
|
|
36
|
+
browser: report.browser,
|
|
37
|
+
})}`,
|
|
38
|
+
`# Evidence\n${bulletList(optionalStrings([
|
|
39
|
+
telemetry.event_id ? `Telemetry event: ${telemetry.event_id}` : null,
|
|
40
|
+
telemetry.replay_id ? `Session replay: ${telemetry.replay_id}` : null,
|
|
41
|
+
telemetry.feedback_id ? `User feedback: ${telemetry.feedback_id}` : null,
|
|
42
|
+
report.screenshot_url ? `Screenshot: ${report.screenshot_url}` : null,
|
|
43
|
+
report.request_id ? `Request ID: ${report.request_id}` : null,
|
|
44
|
+
]))}`,
|
|
45
|
+
`# Reproduction Steps\n${bulletList(report.reproduction_steps)}`,
|
|
46
|
+
`# Expected Behavior\n${report.expected_behavior || "The user can complete the intended workflow without errors."}`,
|
|
47
|
+
`# Actual Behavior\n${report.actual_behavior || summary}`,
|
|
48
|
+
`# Confirmed Facts\n${bulletList(optionalStrings([
|
|
49
|
+
`Report ID ${report.id} was received by Noya.`,
|
|
50
|
+
report.route ? `The report was captured on route ${report.route}.` : null,
|
|
51
|
+
Object.keys(hints).length > 0 ? `Correlation hints were extracted: ${Object.keys(hints).join(", ")}.` : null,
|
|
52
|
+
collectorResult ? "Customer runner returned a redacted context bundle." : null,
|
|
53
|
+
]))}`,
|
|
54
|
+
`# Inferences / Suspected Cause\n${bulletList([
|
|
55
|
+
report.suspected_area
|
|
56
|
+
? `Suspected area from metadata: ${report.suspected_area}.`
|
|
57
|
+
: "Needs engineering investigation; Noya is not asserting root cause.",
|
|
58
|
+
])}`,
|
|
59
|
+
`# Relevant Logs\n${objectBlock(context.logs)}`,
|
|
60
|
+
`# Relevant Domain Snapshot\n${objectBlock(context.domain_snapshot)}`,
|
|
61
|
+
`# Telemetry Breadcrumbs\n${objectBlock(report.context?.sentry_breadcrumbs)}`,
|
|
62
|
+
`# Collector Policy Decisions\n${objectBlock(collectorResult?.policy)}`,
|
|
63
|
+
`# Telemetry Links\n${bulletList(optionalStrings([
|
|
64
|
+
telemetry.event_url ? telemetry.event_url : null,
|
|
65
|
+
telemetry.replay_url ? telemetry.replay_url : null,
|
|
66
|
+
]))}`,
|
|
67
|
+
`# Agent Investigation Hints\n${objectBlock({
|
|
68
|
+
hints,
|
|
69
|
+
frontend_context: report.context ?? {},
|
|
70
|
+
suspected_files: report.suspected_files ?? [],
|
|
71
|
+
repo_path: project.runner?.repo_path,
|
|
72
|
+
})}`,
|
|
73
|
+
`# Definition of Done\n${bulletList([
|
|
74
|
+
"Root cause is confirmed or explicitly ruled out.",
|
|
75
|
+
"Regression test or focused verification is added when practical.",
|
|
76
|
+
"Fix opens a reviewable PR or local draft PR artifact.",
|
|
77
|
+
"No merge, deploy, or production write is performed by Noya.",
|
|
78
|
+
])}`,
|
|
79
|
+
].join("\n\n");
|
|
80
|
+
return { title, body };
|
|
81
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Capabilities, Issue, Job, JsonObject, Project, Report } from "./types.js";
|
|
2
|
+
import type { JsonStore } from "./store.js";
|
|
3
|
+
type ProjectInput = Partial<Project> & Pick<Project, "project_key">;
|
|
4
|
+
type ReportEnvelope = JsonObject & {
|
|
5
|
+
report_id?: string;
|
|
6
|
+
project_key: string;
|
|
7
|
+
message?: string;
|
|
8
|
+
impact?: string;
|
|
9
|
+
environment?: string;
|
|
10
|
+
release?: string;
|
|
11
|
+
route?: string;
|
|
12
|
+
browser?: string;
|
|
13
|
+
request_id?: string;
|
|
14
|
+
screenshot_url?: string;
|
|
15
|
+
telemetry?: Report["telemetry"];
|
|
16
|
+
sentry?: Report["sentry"];
|
|
17
|
+
feedback?: Report["feedback"];
|
|
18
|
+
context?: Report["context"];
|
|
19
|
+
reproduction_steps?: string[];
|
|
20
|
+
expected_behavior?: string;
|
|
21
|
+
actual_behavior?: string;
|
|
22
|
+
suspected_area?: string;
|
|
23
|
+
suspected_files?: string[];
|
|
24
|
+
};
|
|
25
|
+
export declare function receiveSentryWebhook(store: JsonStore, payload: JsonObject): Promise<{
|
|
26
|
+
report: Report;
|
|
27
|
+
job: Job;
|
|
28
|
+
issue: null;
|
|
29
|
+
} | {
|
|
30
|
+
report: Report;
|
|
31
|
+
job: null;
|
|
32
|
+
issue: Issue;
|
|
33
|
+
}>;
|
|
34
|
+
export declare function upsertProject(store: JsonStore, input: ProjectInput): Project;
|
|
35
|
+
export declare function receiveReport(store: JsonStore, envelope: ReportEnvelope): Promise<{
|
|
36
|
+
report: Report;
|
|
37
|
+
job: Job;
|
|
38
|
+
issue: null;
|
|
39
|
+
} | {
|
|
40
|
+
report: Report;
|
|
41
|
+
job: null;
|
|
42
|
+
issue: Issue;
|
|
43
|
+
}>;
|
|
44
|
+
export declare function nextRunnerJob(store: JsonStore, { projectKey, runnerId, capabilities, }: {
|
|
45
|
+
projectKey: string | null;
|
|
46
|
+
runnerId: string;
|
|
47
|
+
capabilities: Capabilities;
|
|
48
|
+
}): Job | null;
|
|
49
|
+
export declare function heartbeatJob(store: JsonStore, jobId: string): Job;
|
|
50
|
+
export declare function completeJob(store: JsonStore, jobId: string, result: JsonObject): Promise<{
|
|
51
|
+
job: Job;
|
|
52
|
+
issue: Issue;
|
|
53
|
+
} | {
|
|
54
|
+
job: Job;
|
|
55
|
+
issue?: undefined;
|
|
56
|
+
}>;
|
|
57
|
+
export declare function failJob(store: JsonStore, jobId: string, error: JsonObject): Job;
|
|
58
|
+
export declare function approveFix(store: JsonStore, issueId: string): Job;
|
|
59
|
+
export {};
|