@ascenda-one/github-collector 0.1.4
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 +81 -0
- package/dist/cli.js +982 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +31 -0
- package/dist/config.js.map +1 -0
- package/dist/mapForgeEvent.js +115 -0
- package/dist/mapForgeEvent.js.map +1 -0
- package/examples/sample-review-requested.json +6 -0
- package/examples/sample-review-submitted.json +8 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @ascenda-one/github-collector
|
|
2
|
+
|
|
3
|
+
The collaboration signal family (consolidated report §4.2): review load and
|
|
4
|
+
pull-request activity, collected from a code forge.
|
|
5
|
+
|
|
6
|
+
## What it emits
|
|
7
|
+
|
|
8
|
+
| Event | Meaning | Workload leg |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| `review_requested_of_me` | someone asked **you** to review | supervision |
|
|
11
|
+
| `review_given` | **you** submitted a review | supervision |
|
|
12
|
+
| `pull_request_opened` | **you** opened a pull request | creation |
|
|
13
|
+
|
|
14
|
+
The two review events are the report's *verification overload* concern — the
|
|
15
|
+
checking burden that concentrates on senior engineers as a team adopts AI.
|
|
16
|
+
|
|
17
|
+
## The rule that shapes the whole package
|
|
18
|
+
|
|
19
|
+
**Only your own activity is ever emitted.** An event is produced when the
|
|
20
|
+
payload says you did the thing or you were asked; a payload about two other
|
|
21
|
+
people produces nothing at all. `ASCENDA_FORGE_LOGIN` is required and the
|
|
22
|
+
collector refuses to run without it, because falling back to the payload's
|
|
23
|
+
actor would silently start recording colleagues.
|
|
24
|
+
|
|
25
|
+
This is not squeamishness. "Who reviews for whom" is a map of a team, and a
|
|
26
|
+
wellbeing rail that assembles one has become a management tool. Concentration
|
|
27
|
+
of checking load is still answerable — it shows up in *your own* supervision
|
|
28
|
+
share, and in cohort aggregates the org rail already suppresses below its
|
|
29
|
+
minimum cohort size.
|
|
30
|
+
|
|
31
|
+
### What never travels
|
|
32
|
+
|
|
33
|
+
No repository name, PR title, branch, PR number, review body, or any other
|
|
34
|
+
person's login. The repository is reduced to an 8-character hash so that "is it
|
|
35
|
+
always the same repository" stays answerable without naming it. There is no
|
|
36
|
+
field on the emitted metadata that a title or a body could be placed in, and a
|
|
37
|
+
test asserts each of those strings is absent from what gets sent.
|
|
38
|
+
|
|
39
|
+
### Withdrawal is not derivable, deliberately
|
|
40
|
+
|
|
41
|
+
There is no "did not review" event and there must never be one. Reviewing less
|
|
42
|
+
is exactly the signal the report says must never be machine-interpreted — a
|
|
43
|
+
quiet week has too many innocent explanations. Nothing here counts absence.
|
|
44
|
+
|
|
45
|
+
## Consent
|
|
46
|
+
|
|
47
|
+
Collaboration events ride **`workflow_telemetry`**, not `ide_telemetry`, and the
|
|
48
|
+
collector has its own tool type (`github_collector`). Both are deliberate: a
|
|
49
|
+
pull request is not an IDE event, and someone may be willing to share how they
|
|
50
|
+
work in their editor and not how they work with their team. The two are
|
|
51
|
+
separately revocable.
|
|
52
|
+
|
|
53
|
+
## Use in GitHub Actions
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
name: ascenda-collaboration
|
|
57
|
+
on:
|
|
58
|
+
pull_request:
|
|
59
|
+
types: [opened, review_requested]
|
|
60
|
+
pull_request_review:
|
|
61
|
+
types: [submitted]
|
|
62
|
+
|
|
63
|
+
jobs:
|
|
64
|
+
collect:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- run: npx @ascenda-one/github-collector
|
|
68
|
+
env:
|
|
69
|
+
ASCENDA_TOOL_INSTALLATION_ID: ${{ secrets.ASCENDA_TOOL_INSTALLATION_ID }}
|
|
70
|
+
ASCENDA_EVENT_WRITE_TOKEN: ${{ secrets.ASCENDA_EVENT_WRITE_TOKEN }}
|
|
71
|
+
ASCENDA_FORGE_LOGIN: ${{ github.actor }}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The step exits 0 on every path that is not a configuration error, including
|
|
75
|
+
"nothing to emit". A telemetry step must never be the reason a build goes red.
|
|
76
|
+
|
|
77
|
+
Locally, pipe a payload instead:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
cat examples/sample-review-submitted.json | ascenda-forge-collect pull_request_review
|
|
81
|
+
```
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
10
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
11
|
+
}) : x)(function(x) {
|
|
12
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
13
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
14
|
+
});
|
|
15
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
16
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
27
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
28
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
29
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
30
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
31
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
32
|
+
mod
|
|
33
|
+
));
|
|
34
|
+
|
|
35
|
+
// ../packages/tool-kit/out/commandClassifier.js
|
|
36
|
+
var require_commandClassifier = __commonJS({
|
|
37
|
+
"../packages/tool-kit/out/commandClassifier.js"(exports) {
|
|
38
|
+
"use strict";
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.classifyCommand = classifyCommand;
|
|
41
|
+
exports.isVerificationCommand = isVerificationCommand;
|
|
42
|
+
function classifyCommand(command) {
|
|
43
|
+
if (!command)
|
|
44
|
+
return "unknown";
|
|
45
|
+
const value = command.toLowerCase().trim();
|
|
46
|
+
if (/\b(test|jest|vitest|mocha|pytest|rspec|go test|cargo test|dotnet test|xcodebuild test)\b/.test(value) || /\bnpm\s+(run\s+)?test\b/.test(value) || /\byarn\s+test\b/.test(value) || /\bpnpm\s+(run\s+)?test\b/.test(value))
|
|
47
|
+
return "test";
|
|
48
|
+
if (/\b(lint|eslint|ruff|flake8|pylint|rubocop)\b/.test(value) || /\bnpm\s+(run\s+)?lint\b/.test(value))
|
|
49
|
+
return "lint";
|
|
50
|
+
if (/\b(tsc|typecheck|mypy|pyright|sorbet|flow)\b/.test(value) || /\bnpm\s+(run\s+)?typecheck\b/.test(value))
|
|
51
|
+
return "typecheck";
|
|
52
|
+
if (/\b(build|webpack|vite build|next build|turbo build|cargo build|go build|dotnet build|xcodebuild)\b/.test(value) || /\bnpm\s+(run\s+)?build\b/.test(value))
|
|
53
|
+
return "build";
|
|
54
|
+
if (/\b(git)\b/.test(value))
|
|
55
|
+
return "git";
|
|
56
|
+
if (/\b(npm install|yarn install|pnpm install|bun install|pip install|poetry install|bundle install)\b/.test(value))
|
|
57
|
+
return "install";
|
|
58
|
+
if (/\b(npm start|npm run dev|yarn dev|pnpm dev|next dev|vite|node|python|tsx|ts-node)\b/.test(value))
|
|
59
|
+
return "run";
|
|
60
|
+
return "unknown";
|
|
61
|
+
}
|
|
62
|
+
function isVerificationCommand(commandClass) {
|
|
63
|
+
return commandClass === "test" || commandClass === "lint" || commandClass === "typecheck" || commandClass === "build";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ../packages/tool-kit/out/gitActionClassifier.js
|
|
69
|
+
var require_gitActionClassifier = __commonJS({
|
|
70
|
+
"../packages/tool-kit/out/gitActionClassifier.js"(exports) {
|
|
71
|
+
"use strict";
|
|
72
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
73
|
+
exports.classifyGitAction = classifyGitAction;
|
|
74
|
+
exports.isReworkGitAction = isReworkGitAction;
|
|
75
|
+
function classifyGitAction(command) {
|
|
76
|
+
if (!command)
|
|
77
|
+
return void 0;
|
|
78
|
+
const value = command.toLowerCase().trim();
|
|
79
|
+
if (!/\bgit\b/.test(value))
|
|
80
|
+
return void 0;
|
|
81
|
+
if (/\bgit\s+commit\b[^\n]*--amend\b/.test(value))
|
|
82
|
+
return "amend";
|
|
83
|
+
if (/\bgit\s+revert\b/.test(value))
|
|
84
|
+
return "revert";
|
|
85
|
+
if (/\bgit\s+reset\b[^\n]*--hard\b/.test(value))
|
|
86
|
+
return "reset_hard";
|
|
87
|
+
if (/\bgit\s+restore\b/.test(value) && !/--staged\b/.test(value))
|
|
88
|
+
return "restore";
|
|
89
|
+
if (/\bgit\s+checkout\b[^\n]*\s--\s/.test(value))
|
|
90
|
+
return "restore";
|
|
91
|
+
if (/\bgit\s+push\b/.test(value))
|
|
92
|
+
return "push";
|
|
93
|
+
if (/\bgit\s+commit\b/.test(value))
|
|
94
|
+
return "commit";
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
function isReworkGitAction(action) {
|
|
98
|
+
return action === "revert" || action === "reset_hard" || action === "restore";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ../packages/tool-kit/out/workMilestoneClassifier.js
|
|
104
|
+
var require_workMilestoneClassifier = __commonJS({
|
|
105
|
+
"../packages/tool-kit/out/workMilestoneClassifier.js"(exports) {
|
|
106
|
+
"use strict";
|
|
107
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
108
|
+
exports.classifyWorkMilestone = classifyWorkMilestone;
|
|
109
|
+
exports.invitesDebrief = invitesDebrief;
|
|
110
|
+
function classifyWorkMilestone(command) {
|
|
111
|
+
if (!command)
|
|
112
|
+
return void 0;
|
|
113
|
+
const value = command.toLowerCase().trim();
|
|
114
|
+
if (!/\bgh\b/.test(value))
|
|
115
|
+
return void 0;
|
|
116
|
+
if (/\bgh\s+pr\s+merge\b/.test(value))
|
|
117
|
+
return "pr_merged";
|
|
118
|
+
if (/\bgh\s+issue\s+close\b/.test(value))
|
|
119
|
+
return "issue_closed";
|
|
120
|
+
if (/\bgh\s+pr\s+create\b/.test(value))
|
|
121
|
+
return "pr_opened";
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
function invitesDebrief(kind) {
|
|
125
|
+
return kind === "pr_merged" || kind === "issue_closed";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ../packages/tool-kit/out/buckets.js
|
|
131
|
+
var require_buckets = __commonJS({
|
|
132
|
+
"../packages/tool-kit/out/buckets.js"(exports) {
|
|
133
|
+
"use strict";
|
|
134
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
135
|
+
exports.bucketLinesChanged = bucketLinesChanged;
|
|
136
|
+
exports.bucketDurationMs = bucketDurationMs;
|
|
137
|
+
function bucketLinesChanged(count) {
|
|
138
|
+
if (count <= 0)
|
|
139
|
+
return "0";
|
|
140
|
+
if (count <= 10)
|
|
141
|
+
return "1-10";
|
|
142
|
+
if (count <= 50)
|
|
143
|
+
return "10-50";
|
|
144
|
+
if (count <= 200)
|
|
145
|
+
return "50-200";
|
|
146
|
+
return "200+";
|
|
147
|
+
}
|
|
148
|
+
function bucketDurationMs(durationMs) {
|
|
149
|
+
if (typeof durationMs !== "number" || !Number.isFinite(durationMs) || durationMs < 0)
|
|
150
|
+
return void 0;
|
|
151
|
+
const minutes = durationMs / 6e4;
|
|
152
|
+
if (minutes <= 1)
|
|
153
|
+
return "0-1m";
|
|
154
|
+
if (minutes <= 5)
|
|
155
|
+
return "1-5m";
|
|
156
|
+
if (minutes <= 10)
|
|
157
|
+
return "5-10m";
|
|
158
|
+
if (minutes <= 30)
|
|
159
|
+
return "10-30m";
|
|
160
|
+
if (minutes <= 60)
|
|
161
|
+
return "30-60m";
|
|
162
|
+
return "60m+";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ../packages/tool-kit/out/afterHours.js
|
|
168
|
+
var require_afterHours = __commonJS({
|
|
169
|
+
"../packages/tool-kit/out/afterHours.js"(exports) {
|
|
170
|
+
"use strict";
|
|
171
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
172
|
+
exports.isAfterHours = isAfterHours;
|
|
173
|
+
function isAfterHours(now = /* @__PURE__ */ new Date(), start = "19:00", end = "07:00") {
|
|
174
|
+
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
|
175
|
+
const startMinutes = parseTimeToMinutes(start, 19 * 60);
|
|
176
|
+
const endMinutes = parseTimeToMinutes(end, 7 * 60);
|
|
177
|
+
if (startMinutes === endMinutes)
|
|
178
|
+
return false;
|
|
179
|
+
if (startMinutes < endMinutes)
|
|
180
|
+
return currentMinutes >= startMinutes && currentMinutes < endMinutes;
|
|
181
|
+
return currentMinutes >= startMinutes || currentMinutes < endMinutes;
|
|
182
|
+
}
|
|
183
|
+
function parseTimeToMinutes(value, fallback) {
|
|
184
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
|
|
185
|
+
if (!match)
|
|
186
|
+
return fallback;
|
|
187
|
+
const hours = Number(match[1]);
|
|
188
|
+
const minutes = Number(match[2]);
|
|
189
|
+
if (Number.isNaN(hours) || Number.isNaN(minutes))
|
|
190
|
+
return fallback;
|
|
191
|
+
return Math.max(0, Math.min(23, hours)) * 60 + Math.max(0, Math.min(59, minutes));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ../packages/tool-kit/out/payload.js
|
|
197
|
+
var require_payload = __commonJS({
|
|
198
|
+
"../packages/tool-kit/out/payload.js"(exports) {
|
|
199
|
+
"use strict";
|
|
200
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
201
|
+
exports.getString = getString;
|
|
202
|
+
exports.getNumber = getNumber;
|
|
203
|
+
exports.getNested = getNested;
|
|
204
|
+
exports.getNestedString = getNestedString;
|
|
205
|
+
exports.getNestedNumber = getNestedNumber;
|
|
206
|
+
exports.inferOutcome = inferOutcome;
|
|
207
|
+
exports.looksLikeCorrection = looksLikeCorrection;
|
|
208
|
+
function getString(input, keys) {
|
|
209
|
+
for (const key of keys) {
|
|
210
|
+
const value = input[key];
|
|
211
|
+
if (typeof value === "string" && value.trim())
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
return void 0;
|
|
215
|
+
}
|
|
216
|
+
function getNumber(input, keys) {
|
|
217
|
+
for (const key of keys) {
|
|
218
|
+
const value = input[key];
|
|
219
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
return void 0;
|
|
223
|
+
}
|
|
224
|
+
function getNested(input, path) {
|
|
225
|
+
let current = input;
|
|
226
|
+
for (const segment of path) {
|
|
227
|
+
if (!current || typeof current !== "object")
|
|
228
|
+
return void 0;
|
|
229
|
+
current = current[segment];
|
|
230
|
+
}
|
|
231
|
+
return current;
|
|
232
|
+
}
|
|
233
|
+
function getNestedString(input, paths) {
|
|
234
|
+
for (const path of paths) {
|
|
235
|
+
const value = getNested(input, path);
|
|
236
|
+
if (typeof value === "string" && value.trim())
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
return void 0;
|
|
240
|
+
}
|
|
241
|
+
function getNestedNumber(input, paths) {
|
|
242
|
+
for (const path of paths) {
|
|
243
|
+
const value = getNested(input, path);
|
|
244
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
return void 0;
|
|
248
|
+
}
|
|
249
|
+
function inferOutcome(input) {
|
|
250
|
+
const exitCode = getNumber(input, ["exitCode", "exit_code", "status"]) ?? getNestedNumber(input, [["tool_response", "exitCode"], ["tool_response", "exit_code"], ["result", "exitCode"], ["result", "exit_code"]]);
|
|
251
|
+
if (typeof exitCode === "number")
|
|
252
|
+
return exitCode === 0 ? "success" : "failure";
|
|
253
|
+
const error = getString(input, ["error", "errorMessage"]) ?? getNestedString(input, [["tool_response", "error"], ["result", "error"]]);
|
|
254
|
+
if (error)
|
|
255
|
+
return "failure";
|
|
256
|
+
return "unknown";
|
|
257
|
+
}
|
|
258
|
+
function looksLikeCorrection(text) {
|
|
259
|
+
if (!text)
|
|
260
|
+
return false;
|
|
261
|
+
return /\b(wrong|incorrect|try again|fix|not what i asked|that's not|that is not|redo|regenerate|you missed|doesn't work|does not work)\b/i.test(text);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ../packages/tool-contract/out/index.js
|
|
267
|
+
var require_out = __commonJS({
|
|
268
|
+
"../packages/tool-contract/out/index.js"(exports) {
|
|
269
|
+
"use strict";
|
|
270
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
271
|
+
exports.ASCENDA_SEMANTIC_PROVENANCE = exports.ASCENDA_COLLABORATION_CONSENT_SCOPE = exports.ASCENDA_SEMANTIC_CONSENT_SCOPE = exports.ASCENDA_PROVENANCE = exports.ASCENDA_CONSENT_SCOPE = exports.EVENT_WORKLOAD_CATEGORY = exports.COLLABORATION_EVENT_TYPES = exports.SEMANTIC_WORK_SIGNAL_EVENT_TYPES = void 0;
|
|
272
|
+
exports.SEMANTIC_WORK_SIGNAL_EVENT_TYPES = [
|
|
273
|
+
"approach_churn_detected",
|
|
274
|
+
"goal_drift_detected",
|
|
275
|
+
"progress_stalled",
|
|
276
|
+
"progress_recovered",
|
|
277
|
+
"session_intention_declared",
|
|
278
|
+
"scope_change_declared"
|
|
279
|
+
];
|
|
280
|
+
exports.COLLABORATION_EVENT_TYPES = [
|
|
281
|
+
"review_requested_of_me",
|
|
282
|
+
"review_given",
|
|
283
|
+
"pull_request_opened"
|
|
284
|
+
];
|
|
285
|
+
exports.EVENT_WORKLOAD_CATEGORY = {
|
|
286
|
+
create_focus_session: "creation",
|
|
287
|
+
ai_prompt_submitted: "creation",
|
|
288
|
+
ai_generation_completed: "creation",
|
|
289
|
+
ai_file_write: "creation",
|
|
290
|
+
ai_file_edit: "creation",
|
|
291
|
+
editor_verification_activity: "verification",
|
|
292
|
+
compile_diagnostic: "verification",
|
|
293
|
+
editor_correction_activity: "supervision",
|
|
294
|
+
ai_correction_prompt: "supervision",
|
|
295
|
+
supervis_meeting_load: "supervision",
|
|
296
|
+
ai_tool_call_started: "supervision",
|
|
297
|
+
ai_tool_call_completed: "supervision",
|
|
298
|
+
ai_tool_call_failed: "supervision",
|
|
299
|
+
// Collaboration (the report's §4.2 collaboration family). Both review
|
|
300
|
+
// events are supervision: being asked to check work, and checking it, are
|
|
301
|
+
// the load the report's "verification overload" concern is about — the one
|
|
302
|
+
// that concentrates on senior engineers as a team adopts AI. Opening a pull
|
|
303
|
+
// request is creation: it is the point your own work leaves your hands.
|
|
304
|
+
review_requested_of_me: "supervision",
|
|
305
|
+
review_given: "supervision",
|
|
306
|
+
pull_request_opened: "creation",
|
|
307
|
+
context_pressure_high: "risk",
|
|
308
|
+
agent_loop_long: "risk",
|
|
309
|
+
after_hours_ai_session: "risk",
|
|
310
|
+
compile_error: "risk",
|
|
311
|
+
tool_failure: "risk",
|
|
312
|
+
recovery_offline_period: "neutral",
|
|
313
|
+
context_compression_manual: "neutral",
|
|
314
|
+
context_compression_auto: "neutral",
|
|
315
|
+
editor_activity: "neutral",
|
|
316
|
+
// Semantic (agent-observed) — see SEMANTIC_WORK_SIGNAL_EVENT_TYPES.
|
|
317
|
+
approach_churn_detected: "risk",
|
|
318
|
+
goal_drift_detected: "risk",
|
|
319
|
+
progress_stalled: "risk",
|
|
320
|
+
progress_recovered: "neutral",
|
|
321
|
+
session_intention_declared: "neutral",
|
|
322
|
+
scope_change_declared: "neutral"
|
|
323
|
+
};
|
|
324
|
+
exports.ASCENDA_CONSENT_SCOPE = "ide_telemetry";
|
|
325
|
+
exports.ASCENDA_PROVENANCE = "ai_work_telemetry";
|
|
326
|
+
exports.ASCENDA_SEMANTIC_CONSENT_SCOPE = "semantic_work_signals";
|
|
327
|
+
exports.ASCENDA_COLLABORATION_CONSENT_SCOPE = "workflow_telemetry";
|
|
328
|
+
exports.ASCENDA_SEMANTIC_PROVENANCE = "semantic_work_signals";
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ../packages/tool-kit/out/http.js
|
|
333
|
+
var require_http = __commonJS({
|
|
334
|
+
"../packages/tool-kit/out/http.js"(exports) {
|
|
335
|
+
"use strict";
|
|
336
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
337
|
+
exports.AscendaApiError = void 0;
|
|
338
|
+
exports.createPairingSession = createPairingSession;
|
|
339
|
+
exports.getPairingStatus = getPairingStatus;
|
|
340
|
+
exports.renewToolToken = renewToolToken;
|
|
341
|
+
exports.postToolEvent = postToolEvent;
|
|
342
|
+
exports.postToolEventsBatch = postToolEventsBatch;
|
|
343
|
+
exports.parseIngestResponse = parseIngestResponse;
|
|
344
|
+
var AscendaApiError = class extends Error {
|
|
345
|
+
status;
|
|
346
|
+
errorCode;
|
|
347
|
+
constructor(status, errorCode, body) {
|
|
348
|
+
super(body ?? `Ascenda API error ${status}`);
|
|
349
|
+
this.status = status;
|
|
350
|
+
this.errorCode = errorCode;
|
|
351
|
+
this.name = "AscendaApiError";
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
exports.AscendaApiError = AscendaApiError;
|
|
355
|
+
async function createPairingSession(apiBaseUrl, toolInstallationId, toolType, displayName) {
|
|
356
|
+
const response = await fetch(`${apiBaseUrl}/v1/tool-pairing-sessions`, {
|
|
357
|
+
method: "POST",
|
|
358
|
+
headers: { "Content-Type": "application/json" },
|
|
359
|
+
body: JSON.stringify({ toolInstallationId, toolType, displayName })
|
|
360
|
+
});
|
|
361
|
+
if (!response.ok)
|
|
362
|
+
throw new AscendaApiError(response.status, void 0, await response.text());
|
|
363
|
+
return await response.json();
|
|
364
|
+
}
|
|
365
|
+
async function getPairingStatus(apiBaseUrl, pairingSessionId) {
|
|
366
|
+
const response = await fetch(`${apiBaseUrl}/v1/tool-pairing-sessions/${encodeURIComponent(pairingSessionId)}/status`, {
|
|
367
|
+
method: "GET",
|
|
368
|
+
headers: { Accept: "application/json" }
|
|
369
|
+
});
|
|
370
|
+
if (!response.ok)
|
|
371
|
+
throw new AscendaApiError(response.status, void 0, await response.text());
|
|
372
|
+
return await response.json();
|
|
373
|
+
}
|
|
374
|
+
async function renewToolToken(apiBaseUrl, eventWriteToken, signal) {
|
|
375
|
+
const response = await fetch(`${apiBaseUrl}/v1/tool-events/renew-token`, {
|
|
376
|
+
method: "POST",
|
|
377
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${eventWriteToken}` },
|
|
378
|
+
signal
|
|
379
|
+
});
|
|
380
|
+
if (response.status === 401)
|
|
381
|
+
return null;
|
|
382
|
+
if (!response.ok)
|
|
383
|
+
throw new AscendaApiError(response.status, void 0, await response.text());
|
|
384
|
+
return await response.json();
|
|
385
|
+
}
|
|
386
|
+
async function postToolEvent(apiBaseUrl, eventWriteToken, payload, signal) {
|
|
387
|
+
const response = await fetch(`${apiBaseUrl}/v1/tool-events`, {
|
|
388
|
+
method: "POST",
|
|
389
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${eventWriteToken}` },
|
|
390
|
+
body: JSON.stringify(payload),
|
|
391
|
+
signal
|
|
392
|
+
});
|
|
393
|
+
return parseIngestResponse(response);
|
|
394
|
+
}
|
|
395
|
+
async function postToolEventsBatch(apiBaseUrl, eventWriteToken, payloads) {
|
|
396
|
+
const response = await fetch(`${apiBaseUrl}/v1/tool-events/batch`, {
|
|
397
|
+
method: "POST",
|
|
398
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${eventWriteToken}` },
|
|
399
|
+
body: JSON.stringify({ events: payloads })
|
|
400
|
+
});
|
|
401
|
+
return parseIngestResponse(response);
|
|
402
|
+
}
|
|
403
|
+
async function parseIngestResponse(response) {
|
|
404
|
+
if (response.ok)
|
|
405
|
+
return "accepted";
|
|
406
|
+
const body = await response.text();
|
|
407
|
+
let errorCode;
|
|
408
|
+
try {
|
|
409
|
+
errorCode = JSON.parse(body).error;
|
|
410
|
+
} catch {
|
|
411
|
+
errorCode = void 0;
|
|
412
|
+
}
|
|
413
|
+
if (response.status === 401)
|
|
414
|
+
return "auth_failed";
|
|
415
|
+
if (response.status === 403 && errorCode === "consent_missing_or_expired")
|
|
416
|
+
return "consent_missing";
|
|
417
|
+
if (response.status === 400 || response.status === 422)
|
|
418
|
+
return "validation_failed";
|
|
419
|
+
throw new AscendaApiError(response.status, errorCode, body);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// ../packages/tool-kit/out/tokenStore.js
|
|
425
|
+
var require_tokenStore = __commonJS({
|
|
426
|
+
"../packages/tool-kit/out/tokenStore.js"(exports) {
|
|
427
|
+
"use strict";
|
|
428
|
+
var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
|
|
429
|
+
if (k2 === void 0) k2 = k;
|
|
430
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
431
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
432
|
+
desc = { enumerable: true, get: function() {
|
|
433
|
+
return m[k];
|
|
434
|
+
} };
|
|
435
|
+
}
|
|
436
|
+
Object.defineProperty(o, k2, desc);
|
|
437
|
+
} : function(o, m, k, k2) {
|
|
438
|
+
if (k2 === void 0) k2 = k;
|
|
439
|
+
o[k2] = m[k];
|
|
440
|
+
});
|
|
441
|
+
var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
|
|
442
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
443
|
+
} : function(o, v) {
|
|
444
|
+
o["default"] = v;
|
|
445
|
+
});
|
|
446
|
+
var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
|
|
447
|
+
var ownKeys = function(o) {
|
|
448
|
+
ownKeys = Object.getOwnPropertyNames || function(o2) {
|
|
449
|
+
var ar = [];
|
|
450
|
+
for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
|
|
451
|
+
return ar;
|
|
452
|
+
};
|
|
453
|
+
return ownKeys(o);
|
|
454
|
+
};
|
|
455
|
+
return function(mod) {
|
|
456
|
+
if (mod && mod.__esModule) return mod;
|
|
457
|
+
var result = {};
|
|
458
|
+
if (mod != null) {
|
|
459
|
+
for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
460
|
+
}
|
|
461
|
+
__setModuleDefault(result, mod);
|
|
462
|
+
return result;
|
|
463
|
+
};
|
|
464
|
+
}();
|
|
465
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
466
|
+
exports.defaultTokenFilePath = defaultTokenFilePath2;
|
|
467
|
+
exports.persistEventWriteToken = persistEventWriteToken2;
|
|
468
|
+
exports.readTokenFile = readTokenFile2;
|
|
469
|
+
var fs = __importStar(__require("fs"));
|
|
470
|
+
var os = __importStar(__require("os"));
|
|
471
|
+
var path = __importStar(__require("path"));
|
|
472
|
+
function defaultTokenFilePath2(toolInstallationId) {
|
|
473
|
+
return path.join(os.homedir(), ".ascenda", "tokens", sanitizeFilePart(toolInstallationId));
|
|
474
|
+
}
|
|
475
|
+
function persistEventWriteToken2(tokenFilePath, token) {
|
|
476
|
+
const dir = path.dirname(tokenFilePath);
|
|
477
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
478
|
+
fs.writeFileSync(tokenFilePath, token, { encoding: "utf8", mode: 384 });
|
|
479
|
+
if (process.platform !== "win32") {
|
|
480
|
+
fs.chmodSync(dir, 448);
|
|
481
|
+
fs.chmodSync(tokenFilePath, 384);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function readTokenFile2(tokenFilePath) {
|
|
485
|
+
try {
|
|
486
|
+
if (!fs.existsSync(tokenFilePath))
|
|
487
|
+
return void 0;
|
|
488
|
+
const value = fs.readFileSync(tokenFilePath, "utf8").trim();
|
|
489
|
+
return value || void 0;
|
|
490
|
+
} catch {
|
|
491
|
+
return void 0;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function sanitizeFilePart(value) {
|
|
495
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// ../packages/tool-kit/out/eventSender.js
|
|
501
|
+
var require_eventSender = __commonJS({
|
|
502
|
+
"../packages/tool-kit/out/eventSender.js"(exports) {
|
|
503
|
+
"use strict";
|
|
504
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
505
|
+
exports.AscendaEventSender = exports.AscendaSemanticEventError = void 0;
|
|
506
|
+
var tool_contract_1 = require_out();
|
|
507
|
+
var http_1 = require_http();
|
|
508
|
+
var tokenStore_1 = require_tokenStore();
|
|
509
|
+
var AscendaSemanticEventError = class extends Error {
|
|
510
|
+
constructor(message) {
|
|
511
|
+
super(message);
|
|
512
|
+
this.name = "AscendaSemanticEventError";
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
exports.AscendaSemanticEventError = AscendaSemanticEventError;
|
|
516
|
+
var AscendaEventSender2 = class {
|
|
517
|
+
config;
|
|
518
|
+
eventWriteToken;
|
|
519
|
+
constructor(config) {
|
|
520
|
+
this.config = config;
|
|
521
|
+
this.eventWriteToken = config.eventWriteToken;
|
|
522
|
+
}
|
|
523
|
+
async send(mapped) {
|
|
524
|
+
const payload = {
|
|
525
|
+
toolInstallationId: this.config.toolInstallationId,
|
|
526
|
+
source: this.config.source,
|
|
527
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
528
|
+
sessionId: this.config.sessionId ?? void 0,
|
|
529
|
+
workspaceHash: this.config.workspaceHash ?? void 0,
|
|
530
|
+
consentScope: tool_contract_1.ASCENDA_CONSENT_SCOPE,
|
|
531
|
+
provenance: tool_contract_1.ASCENDA_PROVENANCE,
|
|
532
|
+
privacyMode: "metadata_only",
|
|
533
|
+
...mapped,
|
|
534
|
+
metadata: mapped.metadata ?? {}
|
|
535
|
+
};
|
|
536
|
+
return this.post(payload);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Sends one of the six agent-observed types (dark-flow-gap-analysis §2.1).
|
|
540
|
+
* Distinct from {@link send} rather than an option on it, because the
|
|
541
|
+
* differences are non-negotiable, not caller preference:
|
|
542
|
+
*
|
|
543
|
+
* - `consentScope`/`provenance` are always the semantic pair — a lease on
|
|
544
|
+
* `ide_telemetry` alone does not cover these.
|
|
545
|
+
* - `severity` is always `"low"`. The emitter has no baseline to judge
|
|
546
|
+
* against; an elevated reading can only come from the backend's own
|
|
547
|
+
* z-scored evaluation, never from this payload.
|
|
548
|
+
* - `metadata.skillVersion` is required by the type, not merely
|
|
549
|
+
* documented, and checked again here in case a caller building the
|
|
550
|
+
* object dynamically bypasses the type system.
|
|
551
|
+
*
|
|
552
|
+
* Rejects locally (never reaches the network) for an eventType outside
|
|
553
|
+
* {@link SEMANTIC_WORK_SIGNAL_EVENT_TYPES} or a missing/blank
|
|
554
|
+
* `skillVersion` — a malformed semantic event is a bug in the caller, not
|
|
555
|
+
* something the backend should have to catch.
|
|
556
|
+
*/
|
|
557
|
+
async sendSemanticSignal(mapped) {
|
|
558
|
+
if (!tool_contract_1.SEMANTIC_WORK_SIGNAL_EVENT_TYPES.includes(mapped.eventType)) {
|
|
559
|
+
throw new AscendaSemanticEventError(`"${mapped.eventType}" is not a semantic work-signal type. Use send() for a deterministic host event.`);
|
|
560
|
+
}
|
|
561
|
+
if (!mapped.metadata.skillVersion || !mapped.metadata.skillVersion.trim()) {
|
|
562
|
+
throw new AscendaSemanticEventError(`metadata.skillVersion is required for semantic event "${mapped.eventType}".`);
|
|
563
|
+
}
|
|
564
|
+
const payload = {
|
|
565
|
+
toolInstallationId: this.config.toolInstallationId,
|
|
566
|
+
source: this.config.source,
|
|
567
|
+
eventType: mapped.eventType,
|
|
568
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
569
|
+
severity: "low",
|
|
570
|
+
sessionId: this.config.sessionId ?? void 0,
|
|
571
|
+
workspaceHash: this.config.workspaceHash ?? void 0,
|
|
572
|
+
consentScope: tool_contract_1.ASCENDA_SEMANTIC_CONSENT_SCOPE,
|
|
573
|
+
provenance: tool_contract_1.ASCENDA_SEMANTIC_PROVENANCE,
|
|
574
|
+
privacyMode: "metadata_only",
|
|
575
|
+
metadata: mapped.metadata
|
|
576
|
+
};
|
|
577
|
+
return this.post(payload);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Sends a collaboration event under `workflow_telemetry`.
|
|
581
|
+
*
|
|
582
|
+
* A separate method rather than an option on {@link send}, for the same
|
|
583
|
+
* reason {@link sendSemanticSignal} is: the consent scope is a property of
|
|
584
|
+
* what the event *is*, and an options bag would let the wrong one be passed
|
|
585
|
+
* by accident. Rejects locally for anything outside
|
|
586
|
+
* {@link COLLABORATION_EVENT_TYPES}.
|
|
587
|
+
*/
|
|
588
|
+
async sendCollaborationSignal(mapped) {
|
|
589
|
+
if (!tool_contract_1.COLLABORATION_EVENT_TYPES.includes(mapped.eventType)) {
|
|
590
|
+
throw new AscendaSemanticEventError(`"${mapped.eventType}" is not a collaboration event type. Use send() for a deterministic host event.`);
|
|
591
|
+
}
|
|
592
|
+
const payload = {
|
|
593
|
+
toolInstallationId: this.config.toolInstallationId,
|
|
594
|
+
source: this.config.source,
|
|
595
|
+
eventType: mapped.eventType,
|
|
596
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
597
|
+
severity: "low",
|
|
598
|
+
sessionId: this.config.sessionId ?? void 0,
|
|
599
|
+
workspaceHash: this.config.workspaceHash ?? void 0,
|
|
600
|
+
consentScope: tool_contract_1.ASCENDA_COLLABORATION_CONSENT_SCOPE,
|
|
601
|
+
provenance: tool_contract_1.ASCENDA_PROVENANCE,
|
|
602
|
+
privacyMode: "metadata_only",
|
|
603
|
+
metadata: mapped.metadata ?? {}
|
|
604
|
+
};
|
|
605
|
+
return this.post(payload);
|
|
606
|
+
}
|
|
607
|
+
async post(payload) {
|
|
608
|
+
let result = await (0, http_1.postToolEvent)(this.config.apiBaseUrl, this.eventWriteToken, payload, this.signal());
|
|
609
|
+
if (result === "auth_failed") {
|
|
610
|
+
const renewed = await this.renewEventToken();
|
|
611
|
+
if (!renewed)
|
|
612
|
+
return "auth_failed";
|
|
613
|
+
result = await (0, http_1.postToolEvent)(this.config.apiBaseUrl, this.eventWriteToken, payload, this.signal());
|
|
614
|
+
}
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
async renewEventToken() {
|
|
618
|
+
const renewed = await (0, http_1.renewToolToken)(this.config.apiBaseUrl, this.eventWriteToken, this.signal());
|
|
619
|
+
if (!renewed)
|
|
620
|
+
return false;
|
|
621
|
+
this.eventWriteToken = renewed.eventWriteToken;
|
|
622
|
+
(0, tokenStore_1.persistEventWriteToken)(this.config.tokenFilePath, renewed.eventWriteToken);
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
signal() {
|
|
626
|
+
return this.config.timeoutMs ? AbortSignal.timeout(this.config.timeoutMs) : void 0;
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
exports.AscendaEventSender = AscendaEventSender2;
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// ../packages/tool-kit/out/salt.js
|
|
634
|
+
var require_salt = __commonJS({
|
|
635
|
+
"../packages/tool-kit/out/salt.js"(exports) {
|
|
636
|
+
"use strict";
|
|
637
|
+
var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
|
|
638
|
+
if (k2 === void 0) k2 = k;
|
|
639
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
640
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
641
|
+
desc = { enumerable: true, get: function() {
|
|
642
|
+
return m[k];
|
|
643
|
+
} };
|
|
644
|
+
}
|
|
645
|
+
Object.defineProperty(o, k2, desc);
|
|
646
|
+
} : function(o, m, k, k2) {
|
|
647
|
+
if (k2 === void 0) k2 = k;
|
|
648
|
+
o[k2] = m[k];
|
|
649
|
+
});
|
|
650
|
+
var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
|
|
651
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
652
|
+
} : function(o, v) {
|
|
653
|
+
o["default"] = v;
|
|
654
|
+
});
|
|
655
|
+
var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
|
|
656
|
+
var ownKeys = function(o) {
|
|
657
|
+
ownKeys = Object.getOwnPropertyNames || function(o2) {
|
|
658
|
+
var ar = [];
|
|
659
|
+
for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
|
|
660
|
+
return ar;
|
|
661
|
+
};
|
|
662
|
+
return ownKeys(o);
|
|
663
|
+
};
|
|
664
|
+
return function(mod) {
|
|
665
|
+
if (mod && mod.__esModule) return mod;
|
|
666
|
+
var result = {};
|
|
667
|
+
if (mod != null) {
|
|
668
|
+
for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
669
|
+
}
|
|
670
|
+
__setModuleDefault(result, mod);
|
|
671
|
+
return result;
|
|
672
|
+
};
|
|
673
|
+
}();
|
|
674
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
675
|
+
exports.machineSaltFilePath = machineSaltFilePath;
|
|
676
|
+
exports.readOrCreateMachineSalt = readOrCreateMachineSalt;
|
|
677
|
+
exports.hashWithMachineSalt = hashWithMachineSalt;
|
|
678
|
+
var crypto = __importStar(__require("crypto"));
|
|
679
|
+
var fs = __importStar(__require("fs"));
|
|
680
|
+
var os = __importStar(__require("os"));
|
|
681
|
+
var path = __importStar(__require("path"));
|
|
682
|
+
function machineSaltFilePath() {
|
|
683
|
+
return path.join(os.homedir(), ".ascenda", "salt");
|
|
684
|
+
}
|
|
685
|
+
var cache = /* @__PURE__ */ new Map();
|
|
686
|
+
function readOrCreateMachineSalt(saltFilePath = machineSaltFilePath()) {
|
|
687
|
+
const hit = cache.get(saltFilePath);
|
|
688
|
+
if (hit)
|
|
689
|
+
return hit;
|
|
690
|
+
const dir = path.dirname(saltFilePath);
|
|
691
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
692
|
+
let salt;
|
|
693
|
+
try {
|
|
694
|
+
salt = crypto.randomBytes(32).toString("hex");
|
|
695
|
+
fs.writeFileSync(saltFilePath, salt, { encoding: "utf8", mode: 384, flag: "wx" });
|
|
696
|
+
} catch (error) {
|
|
697
|
+
if (error.code !== "EEXIST")
|
|
698
|
+
throw error;
|
|
699
|
+
salt = fs.readFileSync(saltFilePath, "utf8").trim();
|
|
700
|
+
}
|
|
701
|
+
if (process.platform !== "win32") {
|
|
702
|
+
fs.chmodSync(dir, 448);
|
|
703
|
+
fs.chmodSync(saltFilePath, 384);
|
|
704
|
+
}
|
|
705
|
+
cache.set(saltFilePath, salt);
|
|
706
|
+
return salt;
|
|
707
|
+
}
|
|
708
|
+
function hashWithMachineSalt(value, saltFilePath) {
|
|
709
|
+
if (!value)
|
|
710
|
+
return null;
|
|
711
|
+
return crypto.createHash("sha256").update(readOrCreateMachineSalt(saltFilePath)).update("\0").update(value).digest("hex").slice(0, 16);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// ../packages/tool-kit/out/index.js
|
|
717
|
+
var require_out2 = __commonJS({
|
|
718
|
+
"../packages/tool-kit/out/index.js"(exports) {
|
|
719
|
+
"use strict";
|
|
720
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
721
|
+
exports.parseIngestResponse = exports.postToolEventsBatch = exports.postToolEvent = exports.renewToolToken = exports.getPairingStatus = exports.createPairingSession = exports.AscendaApiError = exports.hashWithMachineSalt = exports.readOrCreateMachineSalt = exports.machineSaltFilePath = exports.readTokenFile = exports.persistEventWriteToken = exports.defaultTokenFilePath = exports.AscendaSemanticEventError = exports.AscendaEventSender = exports.looksLikeCorrection = exports.inferOutcome = exports.getNestedNumber = exports.getNestedString = exports.getNested = exports.getNumber = exports.getString = exports.isAfterHours = exports.bucketDurationMs = exports.bucketLinesChanged = exports.invitesDebrief = exports.classifyWorkMilestone = exports.isReworkGitAction = exports.classifyGitAction = exports.isVerificationCommand = exports.classifyCommand = void 0;
|
|
722
|
+
var commandClassifier_1 = require_commandClassifier();
|
|
723
|
+
Object.defineProperty(exports, "classifyCommand", { enumerable: true, get: function() {
|
|
724
|
+
return commandClassifier_1.classifyCommand;
|
|
725
|
+
} });
|
|
726
|
+
Object.defineProperty(exports, "isVerificationCommand", { enumerable: true, get: function() {
|
|
727
|
+
return commandClassifier_1.isVerificationCommand;
|
|
728
|
+
} });
|
|
729
|
+
var gitActionClassifier_1 = require_gitActionClassifier();
|
|
730
|
+
Object.defineProperty(exports, "classifyGitAction", { enumerable: true, get: function() {
|
|
731
|
+
return gitActionClassifier_1.classifyGitAction;
|
|
732
|
+
} });
|
|
733
|
+
Object.defineProperty(exports, "isReworkGitAction", { enumerable: true, get: function() {
|
|
734
|
+
return gitActionClassifier_1.isReworkGitAction;
|
|
735
|
+
} });
|
|
736
|
+
var workMilestoneClassifier_1 = require_workMilestoneClassifier();
|
|
737
|
+
Object.defineProperty(exports, "classifyWorkMilestone", { enumerable: true, get: function() {
|
|
738
|
+
return workMilestoneClassifier_1.classifyWorkMilestone;
|
|
739
|
+
} });
|
|
740
|
+
Object.defineProperty(exports, "invitesDebrief", { enumerable: true, get: function() {
|
|
741
|
+
return workMilestoneClassifier_1.invitesDebrief;
|
|
742
|
+
} });
|
|
743
|
+
var buckets_1 = require_buckets();
|
|
744
|
+
Object.defineProperty(exports, "bucketLinesChanged", { enumerable: true, get: function() {
|
|
745
|
+
return buckets_1.bucketLinesChanged;
|
|
746
|
+
} });
|
|
747
|
+
Object.defineProperty(exports, "bucketDurationMs", { enumerable: true, get: function() {
|
|
748
|
+
return buckets_1.bucketDurationMs;
|
|
749
|
+
} });
|
|
750
|
+
var afterHours_1 = require_afterHours();
|
|
751
|
+
Object.defineProperty(exports, "isAfterHours", { enumerable: true, get: function() {
|
|
752
|
+
return afterHours_1.isAfterHours;
|
|
753
|
+
} });
|
|
754
|
+
var payload_1 = require_payload();
|
|
755
|
+
Object.defineProperty(exports, "getString", { enumerable: true, get: function() {
|
|
756
|
+
return payload_1.getString;
|
|
757
|
+
} });
|
|
758
|
+
Object.defineProperty(exports, "getNumber", { enumerable: true, get: function() {
|
|
759
|
+
return payload_1.getNumber;
|
|
760
|
+
} });
|
|
761
|
+
Object.defineProperty(exports, "getNested", { enumerable: true, get: function() {
|
|
762
|
+
return payload_1.getNested;
|
|
763
|
+
} });
|
|
764
|
+
Object.defineProperty(exports, "getNestedString", { enumerable: true, get: function() {
|
|
765
|
+
return payload_1.getNestedString;
|
|
766
|
+
} });
|
|
767
|
+
Object.defineProperty(exports, "getNestedNumber", { enumerable: true, get: function() {
|
|
768
|
+
return payload_1.getNestedNumber;
|
|
769
|
+
} });
|
|
770
|
+
Object.defineProperty(exports, "inferOutcome", { enumerable: true, get: function() {
|
|
771
|
+
return payload_1.inferOutcome;
|
|
772
|
+
} });
|
|
773
|
+
Object.defineProperty(exports, "looksLikeCorrection", { enumerable: true, get: function() {
|
|
774
|
+
return payload_1.looksLikeCorrection;
|
|
775
|
+
} });
|
|
776
|
+
var eventSender_1 = require_eventSender();
|
|
777
|
+
Object.defineProperty(exports, "AscendaEventSender", { enumerable: true, get: function() {
|
|
778
|
+
return eventSender_1.AscendaEventSender;
|
|
779
|
+
} });
|
|
780
|
+
Object.defineProperty(exports, "AscendaSemanticEventError", { enumerable: true, get: function() {
|
|
781
|
+
return eventSender_1.AscendaSemanticEventError;
|
|
782
|
+
} });
|
|
783
|
+
var tokenStore_1 = require_tokenStore();
|
|
784
|
+
Object.defineProperty(exports, "defaultTokenFilePath", { enumerable: true, get: function() {
|
|
785
|
+
return tokenStore_1.defaultTokenFilePath;
|
|
786
|
+
} });
|
|
787
|
+
Object.defineProperty(exports, "persistEventWriteToken", { enumerable: true, get: function() {
|
|
788
|
+
return tokenStore_1.persistEventWriteToken;
|
|
789
|
+
} });
|
|
790
|
+
Object.defineProperty(exports, "readTokenFile", { enumerable: true, get: function() {
|
|
791
|
+
return tokenStore_1.readTokenFile;
|
|
792
|
+
} });
|
|
793
|
+
var salt_1 = require_salt();
|
|
794
|
+
Object.defineProperty(exports, "machineSaltFilePath", { enumerable: true, get: function() {
|
|
795
|
+
return salt_1.machineSaltFilePath;
|
|
796
|
+
} });
|
|
797
|
+
Object.defineProperty(exports, "readOrCreateMachineSalt", { enumerable: true, get: function() {
|
|
798
|
+
return salt_1.readOrCreateMachineSalt;
|
|
799
|
+
} });
|
|
800
|
+
Object.defineProperty(exports, "hashWithMachineSalt", { enumerable: true, get: function() {
|
|
801
|
+
return salt_1.hashWithMachineSalt;
|
|
802
|
+
} });
|
|
803
|
+
var http_1 = require_http();
|
|
804
|
+
Object.defineProperty(exports, "AscendaApiError", { enumerable: true, get: function() {
|
|
805
|
+
return http_1.AscendaApiError;
|
|
806
|
+
} });
|
|
807
|
+
Object.defineProperty(exports, "createPairingSession", { enumerable: true, get: function() {
|
|
808
|
+
return http_1.createPairingSession;
|
|
809
|
+
} });
|
|
810
|
+
Object.defineProperty(exports, "getPairingStatus", { enumerable: true, get: function() {
|
|
811
|
+
return http_1.getPairingStatus;
|
|
812
|
+
} });
|
|
813
|
+
Object.defineProperty(exports, "renewToolToken", { enumerable: true, get: function() {
|
|
814
|
+
return http_1.renewToolToken;
|
|
815
|
+
} });
|
|
816
|
+
Object.defineProperty(exports, "postToolEvent", { enumerable: true, get: function() {
|
|
817
|
+
return http_1.postToolEvent;
|
|
818
|
+
} });
|
|
819
|
+
Object.defineProperty(exports, "postToolEventsBatch", { enumerable: true, get: function() {
|
|
820
|
+
return http_1.postToolEventsBatch;
|
|
821
|
+
} });
|
|
822
|
+
Object.defineProperty(exports, "parseIngestResponse", { enumerable: true, get: function() {
|
|
823
|
+
return http_1.parseIngestResponse;
|
|
824
|
+
} });
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// src/cli.ts
|
|
829
|
+
var import_tool_kit2 = __toESM(require_out2(), 1);
|
|
830
|
+
import { readFile } from "node:fs/promises";
|
|
831
|
+
|
|
832
|
+
// src/config.ts
|
|
833
|
+
var import_tool_kit = __toESM(require_out2(), 1);
|
|
834
|
+
var ASCENDA_TOOL_TYPE = "github_collector";
|
|
835
|
+
function loadConfigFromEnv() {
|
|
836
|
+
const apiBaseUrl = (process.env.ASCENDA_API_BASE_URL ?? "https://api.ascenda.one").replace(/\/$/, "");
|
|
837
|
+
const toolInstallationIdRaw = process.env.ASCENDA_TOOL_INSTALLATION_ID;
|
|
838
|
+
if (!toolInstallationIdRaw) throw new Error("Missing ASCENDA_TOOL_INSTALLATION_ID");
|
|
839
|
+
const toolInstallationId = normalizeToolInstallationId(toolInstallationIdRaw);
|
|
840
|
+
const viewerLogin = process.env.ASCENDA_FORGE_LOGIN?.trim();
|
|
841
|
+
if (!viewerLogin) {
|
|
842
|
+
throw new Error(
|
|
843
|
+
"Missing ASCENDA_FORGE_LOGIN \u2014 the collector only ever emits your own review activity, so it cannot run without knowing whose it is."
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
const tokenFilePath = process.env.ASCENDA_EVENT_WRITE_TOKEN_FILE ?? (0, import_tool_kit.defaultTokenFilePath)(toolInstallationId);
|
|
847
|
+
const fileToken = (0, import_tool_kit.readTokenFile)(tokenFilePath);
|
|
848
|
+
const eventWriteToken = fileToken ?? process.env.ASCENDA_EVENT_WRITE_TOKEN;
|
|
849
|
+
if (!eventWriteToken) throw new Error("Missing ASCENDA_EVENT_WRITE_TOKEN (or token file)");
|
|
850
|
+
if (!fileToken) (0, import_tool_kit.persistEventWriteToken)(tokenFilePath, eventWriteToken);
|
|
851
|
+
return { apiBaseUrl, toolInstallationId, eventWriteToken, tokenFilePath, viewerLogin };
|
|
852
|
+
}
|
|
853
|
+
function normalizeToolInstallationId(value) {
|
|
854
|
+
const trimmed = value.trim();
|
|
855
|
+
return trimmed.includes(":") ? trimmed : `${ASCENDA_TOOL_TYPE}:${trimmed}`;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/mapForgeEvent.ts
|
|
859
|
+
function mapForgeEvent(eventName, payload, viewerLogin) {
|
|
860
|
+
if (!eventName || !viewerLogin) return [];
|
|
861
|
+
const action = str(payload["action"]);
|
|
862
|
+
const viewer = viewerLogin.toLowerCase();
|
|
863
|
+
if (eventName === "pull_request") {
|
|
864
|
+
const pr = obj(payload["pull_request"]);
|
|
865
|
+
const author = str(obj(pr["user"])["login"])?.toLowerCase();
|
|
866
|
+
if (action === "opened" && author === viewer) {
|
|
867
|
+
return [{
|
|
868
|
+
eventType: "pull_request_opened",
|
|
869
|
+
severity: "low",
|
|
870
|
+
metadata: base(payload)
|
|
871
|
+
}];
|
|
872
|
+
}
|
|
873
|
+
if (action === "review_requested") {
|
|
874
|
+
const requested = str(obj(payload["requested_reviewer"])["login"])?.toLowerCase();
|
|
875
|
+
if (requested === viewer) {
|
|
876
|
+
return [{
|
|
877
|
+
eventType: "review_requested_of_me",
|
|
878
|
+
severity: "low",
|
|
879
|
+
metadata: base(payload)
|
|
880
|
+
}];
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return [];
|
|
884
|
+
}
|
|
885
|
+
if (eventName === "pull_request_review" && action === "submitted") {
|
|
886
|
+
const review = obj(payload["review"]);
|
|
887
|
+
const reviewer = str(obj(review["user"])["login"])?.toLowerCase();
|
|
888
|
+
if (reviewer !== viewer) return [];
|
|
889
|
+
return [{
|
|
890
|
+
eventType: "review_given",
|
|
891
|
+
severity: "low",
|
|
892
|
+
metadata: {
|
|
893
|
+
...base(payload),
|
|
894
|
+
// The verdict is a property of the checking work, not of the author,
|
|
895
|
+
// and it is the closest thing to a "how heavy was this review" signal
|
|
896
|
+
// that carries no content. `commented` and `changes_requested` are
|
|
897
|
+
// more work than `approved`.
|
|
898
|
+
outcome: reviewState(str(review["state"]))
|
|
899
|
+
}
|
|
900
|
+
}];
|
|
901
|
+
}
|
|
902
|
+
return [];
|
|
903
|
+
}
|
|
904
|
+
function base(payload) {
|
|
905
|
+
const repo = str(obj(payload["repository"])["full_name"]);
|
|
906
|
+
return {
|
|
907
|
+
host: "github",
|
|
908
|
+
// Hashed, never the name. "Is it always the same repository" stays
|
|
909
|
+
// answerable; which repository does not travel.
|
|
910
|
+
...repo ? { projectHash: hash(repo) } : {}
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function reviewState(state) {
|
|
914
|
+
return state?.toLowerCase() === "approved" ? "success" : "unknown";
|
|
915
|
+
}
|
|
916
|
+
function hash(value) {
|
|
917
|
+
let h = 2166136261;
|
|
918
|
+
for (let i = 0; i < value.length; i++) {
|
|
919
|
+
h ^= value.charCodeAt(i);
|
|
920
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
921
|
+
}
|
|
922
|
+
return h.toString(16).padStart(8, "0");
|
|
923
|
+
}
|
|
924
|
+
function obj(value) {
|
|
925
|
+
return value && typeof value === "object" ? value : {};
|
|
926
|
+
}
|
|
927
|
+
function str(value) {
|
|
928
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/cli.ts
|
|
932
|
+
async function main() {
|
|
933
|
+
const config = loadConfigFromEnv();
|
|
934
|
+
const eventName = process.env.GITHUB_EVENT_NAME ?? process.argv[2];
|
|
935
|
+
const payload = await readPayload();
|
|
936
|
+
if (!payload) return;
|
|
937
|
+
const events = mapForgeEvent(eventName, payload, config.viewerLogin);
|
|
938
|
+
if (events.length === 0) return;
|
|
939
|
+
const sender = new import_tool_kit2.AscendaEventSender({
|
|
940
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
941
|
+
toolInstallationId: config.toolInstallationId,
|
|
942
|
+
source: "code_forge",
|
|
943
|
+
eventWriteToken: config.eventWriteToken,
|
|
944
|
+
tokenFilePath: config.tokenFilePath
|
|
945
|
+
});
|
|
946
|
+
for (const event of events) {
|
|
947
|
+
const result = await sender.sendCollaborationSignal(event);
|
|
948
|
+
if (result === "consent_missing") {
|
|
949
|
+
console.error("Ascenda telemetry rejected: renew workflow telemetry consent in the Ascenda app.");
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (result === "auth_failed") {
|
|
953
|
+
console.error("Ascenda telemetry rejected: event write token invalid or revoked.");
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (result !== "accepted") {
|
|
957
|
+
console.error(`Ascenda telemetry rejected: ${result}`);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
async function readPayload() {
|
|
963
|
+
const path = process.env.GITHUB_EVENT_PATH;
|
|
964
|
+
const raw = path ? await readFile(path, "utf8") : await readStdin();
|
|
965
|
+
if (!raw.trim()) return void 0;
|
|
966
|
+
try {
|
|
967
|
+
const parsed = JSON.parse(raw);
|
|
968
|
+
return parsed && typeof parsed === "object" ? parsed : void 0;
|
|
969
|
+
} catch {
|
|
970
|
+
console.error("Ascenda collector: unreadable event payload, nothing emitted.");
|
|
971
|
+
return void 0;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
async function readStdin() {
|
|
975
|
+
const chunks = [];
|
|
976
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
977
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
978
|
+
}
|
|
979
|
+
main().catch((error) => {
|
|
980
|
+
console.error(`Ascenda collector: ${error instanceof Error ? error.message : String(error)}`);
|
|
981
|
+
process.exit(0);
|
|
982
|
+
});
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,aAAa,EAAgB,MAAM,oBAAoB,CAAC;AAEjE;;;;;;;;;GASG;AACH,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAC;IAEnC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACnE,MAAM,OAAO,GAAG,MAAM,WAAW,EAAE,CAAC;IACpC,IAAI,CAAC,OAAO;QAAE,OAAO;IAErB,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IACrE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAEhC,MAAM,MAAM,GAAG,IAAI,kBAAkB,CAAC;QACpC,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,kBAAkB,EAAE,MAAM,CAAC,kBAAkB;QAC7C,MAAM,EAAE,YAAY;QACpB,eAAe,EAAE,MAAM,CAAC,eAAe;QACvC,aAAa,EAAE,MAAM,CAAC,aAAa;KACpC,CAAC,CAAC;IAEH,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC;QAC3D,IAAI,MAAM,KAAK,iBAAiB,EAAE,CAAC;YACjC,OAAO,CAAC,KAAK,CAAC,kFAAkF,CAAC,CAAC;YAClG,OAAO;QACT,CAAC;QACD,IAAI,MAAM,KAAK,aAAa,EAAE,CAAC;YAC7B,OAAO,CAAC,KAAK,CAAC,mEAAmE,CAAC,CAAC;YACnF,OAAO;QACT,CAAC;QACD,IAAI,MAAM,KAAK,UAAU,EAAE,CAAC;YAC1B,OAAO,CAAC,KAAK,CAAC,+BAA+B,MAAM,EAAE,CAAC,CAAC;YACvD,OAAO;QACT,CAAC;IACH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,WAAW;IACxB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;IAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,SAAS,EAAE,CAAC;IACpE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAAE,OAAO,SAAS,CAAC;IAClC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,OAAO,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAE,MAAuB,CAAC,CAAC,CAAC,SAAS,CAAC;IACrF,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;QACxE,OAAO,CAAC,KAAK,CAAC,+DAA+D,CAAC,CAAC;QAC/E,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,SAAS;IACtB,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK;QAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACzE,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,sBAAsB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC9F,2EAA2E;IAC3E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defaultTokenFilePath, persistEventWriteToken, readTokenFile } from "@ascenda-one/tool-kit";
|
|
2
|
+
export const ASCENDA_TOOL_TYPE = "github_collector";
|
|
3
|
+
export function loadConfigFromEnv() {
|
|
4
|
+
const apiBaseUrl = (process.env.ASCENDA_API_BASE_URL ?? "https://api.ascenda.one").replace(/\/$/, "");
|
|
5
|
+
const toolInstallationIdRaw = process.env.ASCENDA_TOOL_INSTALLATION_ID;
|
|
6
|
+
if (!toolInstallationIdRaw)
|
|
7
|
+
throw new Error("Missing ASCENDA_TOOL_INSTALLATION_ID");
|
|
8
|
+
const toolInstallationId = normalizeToolInstallationId(toolInstallationIdRaw);
|
|
9
|
+
// The whole collector is first-person, so without knowing who "I" am there
|
|
10
|
+
// is nothing it may legitimately emit. Failing here is the point: defaulting
|
|
11
|
+
// to the payload's actor would silently start recording other people.
|
|
12
|
+
const viewerLogin = process.env.ASCENDA_FORGE_LOGIN?.trim();
|
|
13
|
+
if (!viewerLogin) {
|
|
14
|
+
throw new Error("Missing ASCENDA_FORGE_LOGIN — the collector only ever emits your own " +
|
|
15
|
+
"review activity, so it cannot run without knowing whose it is.");
|
|
16
|
+
}
|
|
17
|
+
const tokenFilePath = process.env.ASCENDA_EVENT_WRITE_TOKEN_FILE
|
|
18
|
+
?? defaultTokenFilePath(toolInstallationId);
|
|
19
|
+
const fileToken = readTokenFile(tokenFilePath);
|
|
20
|
+
const eventWriteToken = fileToken ?? process.env.ASCENDA_EVENT_WRITE_TOKEN;
|
|
21
|
+
if (!eventWriteToken)
|
|
22
|
+
throw new Error("Missing ASCENDA_EVENT_WRITE_TOKEN (or token file)");
|
|
23
|
+
if (!fileToken)
|
|
24
|
+
persistEventWriteToken(tokenFilePath, eventWriteToken);
|
|
25
|
+
return { apiBaseUrl, toolInstallationId, eventWriteToken, tokenFilePath, viewerLogin };
|
|
26
|
+
}
|
|
27
|
+
function normalizeToolInstallationId(value) {
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
return trimmed.includes(":") ? trimmed : `${ASCENDA_TOOL_TYPE}:${trimmed}`;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEpG,MAAM,CAAC,MAAM,iBAAiB,GAAG,kBAAkB,CAAC;AAUpD,MAAM,UAAU,iBAAiB;IAC/B,MAAM,UAAU,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,yBAAyB,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAEtG,MAAM,qBAAqB,GAAG,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC;IACvE,IAAI,CAAC,qBAAqB;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IACpF,MAAM,kBAAkB,GAAG,2BAA2B,CAAC,qBAAqB,CAAC,CAAC;IAE9E,2EAA2E;IAC3E,6EAA6E;IAC7E,sEAAsE;IACtE,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAI,EAAE,CAAC;IAC5D,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,uEAAuE;YACvE,gEAAgE,CACjE,CAAC;IACJ,CAAC;IAED,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,8BAA8B;WAC3D,oBAAoB,CAAC,kBAAkB,CAAC,CAAC;IAE9C,MAAM,SAAS,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC;IAC/C,MAAM,eAAe,GAAG,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC;IAC3E,IAAI,CAAC,eAAe;QAAE,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IAC3F,IAAI,CAAC,SAAS;QAAE,sBAAsB,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC;IAEvE,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,eAAe,EAAE,aAAa,EAAE,WAAW,EAAE,CAAC;AACzF,CAAC;AAED,SAAS,2BAA2B,CAAC,KAAa;IAChD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,iBAAiB,IAAI,OAAO,EAAE,CAAC;AAC7E,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps a code-forge event to Ascenda's collaboration events (§4.2's
|
|
3
|
+
* collaboration family).
|
|
4
|
+
*
|
|
5
|
+
* ## The one rule that shapes everything here
|
|
6
|
+
*
|
|
7
|
+
* **Only the viewer's own activity is ever emitted.** `viewerLogin` is the
|
|
8
|
+
* person whose installation this is, and an event is produced only when the
|
|
9
|
+
* payload says *they* did the thing or *they* were asked. A payload about two
|
|
10
|
+
* other people produces nothing at all.
|
|
11
|
+
*
|
|
12
|
+
* That is not squeamishness. "Who reviews for whom" is a map of a team, and a
|
|
13
|
+
* wellbeing rail that assembles one has become a management tool. The report's
|
|
14
|
+
* verification-overload concern — checking load concentrating on senior
|
|
15
|
+
* engineers — is still answerable, because it shows up in that person's own
|
|
16
|
+
* supervision share and in cohort aggregates the org rail already suppresses
|
|
17
|
+
* below its minimum cohort size.
|
|
18
|
+
*
|
|
19
|
+
* ## What never travels
|
|
20
|
+
*
|
|
21
|
+
* No repository name, PR title, branch name, PR number, or any other person's
|
|
22
|
+
* login. The repository is reduced to an opaque hash so that "always the same
|
|
23
|
+
* repo" stays answerable without naming it, and nothing else about the change
|
|
24
|
+
* is carried. There is no field on the emitted metadata that a title or a body
|
|
25
|
+
* could be placed in.
|
|
26
|
+
*/
|
|
27
|
+
export function mapForgeEvent(eventName, payload, viewerLogin) {
|
|
28
|
+
if (!eventName || !viewerLogin)
|
|
29
|
+
return [];
|
|
30
|
+
const action = str(payload["action"]);
|
|
31
|
+
const viewer = viewerLogin.toLowerCase();
|
|
32
|
+
if (eventName === "pull_request") {
|
|
33
|
+
const pr = obj(payload["pull_request"]);
|
|
34
|
+
const author = str(obj(pr["user"])["login"])?.toLowerCase();
|
|
35
|
+
if (action === "opened" && author === viewer) {
|
|
36
|
+
return [{
|
|
37
|
+
eventType: "pull_request_opened",
|
|
38
|
+
severity: "low",
|
|
39
|
+
metadata: base(payload)
|
|
40
|
+
}];
|
|
41
|
+
}
|
|
42
|
+
// A review request naming the viewer. The requester is not recorded: what
|
|
43
|
+
// matters to this rail is that checking work arrived, not who sent it.
|
|
44
|
+
if (action === "review_requested") {
|
|
45
|
+
const requested = str(obj(payload["requested_reviewer"])["login"])?.toLowerCase();
|
|
46
|
+
if (requested === viewer) {
|
|
47
|
+
return [{
|
|
48
|
+
eventType: "review_requested_of_me",
|
|
49
|
+
severity: "low",
|
|
50
|
+
metadata: base(payload)
|
|
51
|
+
}];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
if (eventName === "pull_request_review" && action === "submitted") {
|
|
57
|
+
const review = obj(payload["review"]);
|
|
58
|
+
const reviewer = str(obj(review["user"])["login"])?.toLowerCase();
|
|
59
|
+
if (reviewer !== viewer)
|
|
60
|
+
return [];
|
|
61
|
+
return [{
|
|
62
|
+
eventType: "review_given",
|
|
63
|
+
severity: "low",
|
|
64
|
+
metadata: {
|
|
65
|
+
...base(payload),
|
|
66
|
+
// The verdict is a property of the checking work, not of the author,
|
|
67
|
+
// and it is the closest thing to a "how heavy was this review" signal
|
|
68
|
+
// that carries no content. `commented` and `changes_requested` are
|
|
69
|
+
// more work than `approved`.
|
|
70
|
+
outcome: reviewState(str(review["state"]))
|
|
71
|
+
}
|
|
72
|
+
}];
|
|
73
|
+
}
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
function base(payload) {
|
|
77
|
+
const repo = str(obj(payload["repository"])["full_name"]);
|
|
78
|
+
return {
|
|
79
|
+
host: "github",
|
|
80
|
+
// Hashed, never the name. "Is it always the same repository" stays
|
|
81
|
+
// answerable; which repository does not travel.
|
|
82
|
+
...(repo ? { projectHash: hash(repo) } : {})
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A review verdict, mapped onto the shared outcome vocabulary. An approval is
|
|
87
|
+
* a success; anything asking for more work is not a *failure* of the reviewer,
|
|
88
|
+
* so it maps to unknown rather than borrowing a word that would read as blame
|
|
89
|
+
* in an aggregate.
|
|
90
|
+
*/
|
|
91
|
+
function reviewState(state) {
|
|
92
|
+
return state?.toLowerCase() === "approved" ? "success" : "unknown";
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* FNV-1a. Not a security boundary and not pretending to be one — the
|
|
96
|
+
* repository name is low-entropy and a determined holder of the data could
|
|
97
|
+
* guess it. Its job is to keep names out of the payload and stable across
|
|
98
|
+
* events, which is what makes "the same repository" answerable without
|
|
99
|
+
* recording which one.
|
|
100
|
+
*/
|
|
101
|
+
function hash(value) {
|
|
102
|
+
let h = 0x811c9dc5;
|
|
103
|
+
for (let i = 0; i < value.length; i++) {
|
|
104
|
+
h ^= value.charCodeAt(i);
|
|
105
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
106
|
+
}
|
|
107
|
+
return h.toString(16).padStart(8, "0");
|
|
108
|
+
}
|
|
109
|
+
function obj(value) {
|
|
110
|
+
return value && typeof value === "object" ? value : {};
|
|
111
|
+
}
|
|
112
|
+
function str(value) {
|
|
113
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=mapForgeEvent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mapForgeEvent.js","sourceRoot":"","sources":["../src/mapForgeEvent.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"publishConfig": { "access": "public" },
|
|
3
|
+
"name": "@ascenda-one/github-collector",
|
|
4
|
+
"version": "0.1.4",
|
|
5
|
+
"description": "Code-forge collaboration collector for Ascenda work telemetry — first-person review and pull-request signals.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/ascendaone-com/ai-engineer-tools.git",
|
|
9
|
+
"directory": "ascenda-github-collector"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"files": ["dist", "examples"],
|
|
13
|
+
"bin": {
|
|
14
|
+
"ascenda-forge-collect": "./dist/cli.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p ./ && npm run bundle",
|
|
18
|
+
"watch": "tsc -w -p ./",
|
|
19
|
+
"bundle": "esbuild src/cli.ts --bundle --platform=node --format=esm --banner:js=\"import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);\" --outfile=dist/cli.js",
|
|
20
|
+
"test": "node --test"
|
|
21
|
+
},
|
|
22
|
+
"license": "Apache-2.0",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^20.14.10",
|
|
28
|
+
"typescript": "^5.5.0",
|
|
29
|
+
"@ascenda-one/tool-contract": "*",
|
|
30
|
+
"@ascenda-one/tool-kit": "*"
|
|
31
|
+
}
|
|
32
|
+
}
|