@asc-agent/runtime 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.
Files changed (210) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +22 -0
  3. package/dist/adapters/claude-code/binding.d.ts +5 -0
  4. package/dist/adapters/claude-code/binding.js +14 -0
  5. package/dist/adapters/claude-code/guard.d.ts +54 -0
  6. package/dist/adapters/claude-code/guard.js +295 -0
  7. package/dist/adapters/claude-code/install.d.ts +67 -0
  8. package/dist/adapters/claude-code/install.js +231 -0
  9. package/dist/adapters/claude-code/observer.d.ts +38 -0
  10. package/dist/adapters/claude-code/observer.js +77 -0
  11. package/dist/adapters/claude-code/probe.d.ts +53 -0
  12. package/dist/adapters/claude-code/probe.js +129 -0
  13. package/dist/adapters/claude-code/skill.d.ts +8 -0
  14. package/dist/adapters/claude-code/skill.js +267 -0
  15. package/dist/adapters/fixture-work/index.d.ts +34 -0
  16. package/dist/adapters/fixture-work/index.js +78 -0
  17. package/dist/adapters/github/adapter.d.ts +32 -0
  18. package/dist/adapters/github/adapter.js +95 -0
  19. package/dist/adapters/github/client.d.ts +30 -0
  20. package/dist/adapters/github/client.js +90 -0
  21. package/dist/adapters/github/context.d.ts +44 -0
  22. package/dist/adapters/github/context.js +170 -0
  23. package/dist/adapters/github/event-source.d.ts +55 -0
  24. package/dist/adapters/github/event-source.js +183 -0
  25. package/dist/adapters/github/scm.d.ts +50 -0
  26. package/dist/adapters/github/scm.js +104 -0
  27. package/dist/adapters/gitlab/adapter.d.ts +36 -0
  28. package/dist/adapters/gitlab/adapter.js +117 -0
  29. package/dist/adapters/gitlab/client.d.ts +30 -0
  30. package/dist/adapters/gitlab/client.js +47 -0
  31. package/dist/adapters/gitlab/ports.d.ts +40 -0
  32. package/dist/adapters/gitlab/ports.js +186 -0
  33. package/dist/adapters/jam/adapter.d.ts +53 -0
  34. package/dist/adapters/jam/adapter.js +144 -0
  35. package/dist/adapters/jam/event-source.d.ts +28 -0
  36. package/dist/adapters/jam/event-source.js +65 -0
  37. package/dist/adapters/jam/mcp-client.d.ts +50 -0
  38. package/dist/adapters/jam/mcp-client.js +218 -0
  39. package/dist/adapters/jam/ports.d.ts +51 -0
  40. package/dist/adapters/jam/ports.js +167 -0
  41. package/dist/adapters/local/identity.d.ts +27 -0
  42. package/dist/adapters/local/identity.js +40 -0
  43. package/dist/adapters/local/presentation.d.ts +14 -0
  44. package/dist/adapters/local/presentation.js +42 -0
  45. package/dist/adapters/markdown/layout.d.ts +15 -0
  46. package/dist/adapters/markdown/layout.js +49 -0
  47. package/dist/adapters/markdown/serialize.d.ts +9 -0
  48. package/dist/adapters/markdown/serialize.js +120 -0
  49. package/dist/adapters/markdown/state-store.d.ts +23 -0
  50. package/dist/adapters/markdown/state-store.js +316 -0
  51. package/dist/adapters/mattermost/client.d.ts +23 -0
  52. package/dist/adapters/mattermost/client.js +54 -0
  53. package/dist/adapters/mattermost/presentation.d.ts +43 -0
  54. package/dist/adapters/mattermost/presentation.js +88 -0
  55. package/dist/adapters/memory/mocks.d.ts +87 -0
  56. package/dist/adapters/memory/mocks.js +163 -0
  57. package/dist/adapters/memory/runtime-binding.d.ts +23 -0
  58. package/dist/adapters/memory/runtime-binding.js +118 -0
  59. package/dist/adapters/memory/state-store.d.ts +17 -0
  60. package/dist/adapters/memory/state-store.js +125 -0
  61. package/dist/adapters/text/renderer.d.ts +7 -0
  62. package/dist/adapters/text/renderer.js +81 -0
  63. package/dist/adapters/webhook/ingress.d.ts +103 -0
  64. package/dist/adapters/webhook/ingress.js +150 -0
  65. package/dist/cli/asc.d.ts +16 -0
  66. package/dist/cli/asc.js +2914 -0
  67. package/dist/cli/identity-config.d.ts +7 -0
  68. package/dist/cli/identity-config.js +31 -0
  69. package/dist/composition/observe.d.ts +21 -0
  70. package/dist/composition/observe.js +72 -0
  71. package/dist/composition/registry.d.ts +33 -0
  72. package/dist/composition/registry.js +62 -0
  73. package/dist/composition/runtime.d.ts +63 -0
  74. package/dist/composition/runtime.js +155 -0
  75. package/dist/core/approval/service.d.ts +17 -0
  76. package/dist/core/approval/service.js +129 -0
  77. package/dist/core/attach/bootstrap.d.ts +102 -0
  78. package/dist/core/attach/bootstrap.js +178 -0
  79. package/dist/core/attach/init.d.ts +29 -0
  80. package/dist/core/attach/init.js +100 -0
  81. package/dist/core/attach/setup-plan.d.ts +125 -0
  82. package/dist/core/attach/setup-plan.js +177 -0
  83. package/dist/core/attach/setup.d.ts +40 -0
  84. package/dist/core/attach/setup.js +140 -0
  85. package/dist/core/binding/types.d.ts +93 -0
  86. package/dist/core/binding/types.js +74 -0
  87. package/dist/core/distribution/release.d.ts +15 -0
  88. package/dist/core/distribution/release.js +27 -0
  89. package/dist/core/distribution/runtime-install.d.ts +50 -0
  90. package/dist/core/distribution/runtime-install.js +90 -0
  91. package/dist/core/distribution/runtime-select.d.ts +74 -0
  92. package/dist/core/distribution/runtime-select.js +149 -0
  93. package/dist/core/execution/executor.d.ts +51 -0
  94. package/dist/core/execution/executor.js +106 -0
  95. package/dist/core/execution/grant.d.ts +42 -0
  96. package/dist/core/execution/grant.js +78 -0
  97. package/dist/core/model/entities.d.ts +872 -0
  98. package/dist/core/model/entities.js +285 -0
  99. package/dist/core/model/ids.d.ts +41 -0
  100. package/dist/core/model/ids.js +50 -0
  101. package/dist/core/model/transitions.d.ts +26 -0
  102. package/dist/core/model/transitions.js +124 -0
  103. package/dist/core/monitor/coverage.d.ts +111 -0
  104. package/dist/core/monitor/coverage.js +129 -0
  105. package/dist/core/monitor/engine.d.ts +134 -0
  106. package/dist/core/monitor/engine.js +574 -0
  107. package/dist/core/monitor/health-alerts.d.ts +35 -0
  108. package/dist/core/monitor/health-alerts.js +88 -0
  109. package/dist/core/monitor/investigation.d.ts +82 -0
  110. package/dist/core/monitor/investigation.js +232 -0
  111. package/dist/core/monitor/observation.d.ts +79 -0
  112. package/dist/core/monitor/observation.js +110 -0
  113. package/dist/core/monitor/relevance.d.ts +47 -0
  114. package/dist/core/monitor/relevance.js +100 -0
  115. package/dist/core/monitor/signals.d.ts +59 -0
  116. package/dist/core/monitor/signals.js +105 -0
  117. package/dist/core/operator/local-operator.d.ts +60 -0
  118. package/dist/core/operator/local-operator.js +82 -0
  119. package/dist/core/operator/preflight.d.ts +60 -0
  120. package/dist/core/operator/preflight.js +139 -0
  121. package/dist/core/operator/proceed.d.ts +98 -0
  122. package/dist/core/operator/proceed.js +167 -0
  123. package/dist/core/operator/progress.d.ts +124 -0
  124. package/dist/core/operator/progress.js +135 -0
  125. package/dist/core/operator/render.d.ts +34 -0
  126. package/dist/core/operator/render.js +170 -0
  127. package/dist/core/operator/runtime-binding.d.ts +61 -0
  128. package/dist/core/operator/runtime-binding.js +22 -0
  129. package/dist/core/policy/ownership.d.ts +44 -0
  130. package/dist/core/policy/ownership.js +46 -0
  131. package/dist/core/policy/policy.d.ts +99 -0
  132. package/dist/core/policy/policy.js +147 -0
  133. package/dist/core/policy/remote-freeze.d.ts +101 -0
  134. package/dist/core/policy/remote-freeze.js +151 -0
  135. package/dist/core/policy/scope.d.ts +23 -0
  136. package/dist/core/policy/scope.js +93 -0
  137. package/dist/core/presentation/digest.d.ts +98 -0
  138. package/dist/core/presentation/digest.js +160 -0
  139. package/dist/core/resolver/load.d.ts +121 -0
  140. package/dist/core/resolver/load.js +246 -0
  141. package/dist/core/resolver/render.d.ts +4 -0
  142. package/dist/core/resolver/render.js +62 -0
  143. package/dist/core/resolver/resolve.d.ts +42 -0
  144. package/dist/core/resolver/resolve.js +51 -0
  145. package/dist/core/resolver/version.d.ts +22 -0
  146. package/dist/core/resolver/version.js +49 -0
  147. package/dist/core/runtime/audit.d.ts +374 -0
  148. package/dist/core/runtime/audit.js +454 -0
  149. package/dist/core/runtime/claims.d.ts +115 -0
  150. package/dist/core/runtime/claims.js +153 -0
  151. package/dist/core/runtime/closure.d.ts +73 -0
  152. package/dist/core/runtime/closure.js +162 -0
  153. package/dist/core/runtime/controller.d.ts +40 -0
  154. package/dist/core/runtime/controller.js +121 -0
  155. package/dist/core/runtime/escalation.d.ts +188 -0
  156. package/dist/core/runtime/escalation.js +322 -0
  157. package/dist/core/runtime/execution-state.d.ts +43 -0
  158. package/dist/core/runtime/execution-state.js +81 -0
  159. package/dist/core/runtime/front.d.ts +95 -0
  160. package/dist/core/runtime/front.js +144 -0
  161. package/dist/core/runtime/orchestrator.d.ts +54 -0
  162. package/dist/core/runtime/orchestrator.js +98 -0
  163. package/dist/core/runtime/query.d.ts +184 -0
  164. package/dist/core/runtime/query.js +213 -0
  165. package/dist/core/runtime/report.d.ts +33 -0
  166. package/dist/core/runtime/report.js +108 -0
  167. package/dist/core/runtime/session.d.ts +156 -0
  168. package/dist/core/runtime/session.js +281 -0
  169. package/dist/core/runtime/store-ops.d.ts +22 -0
  170. package/dist/core/runtime/store-ops.js +26 -0
  171. package/dist/core/view/build-view.d.ts +36 -0
  172. package/dist/core/view/build-view.js +131 -0
  173. package/dist/core/view/decision-view.d.ts +564 -0
  174. package/dist/core/view/decision-view.js +103 -0
  175. package/dist/core/workspace/identity.d.ts +82 -0
  176. package/dist/core/workspace/identity.js +133 -0
  177. package/dist/core/workspace/index-store.d.ts +140 -0
  178. package/dist/core/workspace/index-store.js +125 -0
  179. package/dist/core/workspace/migrate.d.ts +73 -0
  180. package/dist/core/workspace/migrate.js +123 -0
  181. package/dist/core/workspace/resolve.d.ts +34 -0
  182. package/dist/core/workspace/resolve.js +89 -0
  183. package/dist/ports/adapter.d.ts +35 -0
  184. package/dist/ports/adapter.js +12 -0
  185. package/dist/ports/approval.d.ts +98 -0
  186. package/dist/ports/approval.js +7 -0
  187. package/dist/ports/change-context.d.ts +22 -0
  188. package/dist/ports/change-context.js +8 -0
  189. package/dist/ports/event-source.d.ts +42 -0
  190. package/dist/ports/event-source.js +6 -0
  191. package/dist/ports/inventory.d.ts +53 -0
  192. package/dist/ports/inventory.js +12 -0
  193. package/dist/ports/presentation.d.ts +62 -0
  194. package/dist/ports/presentation.js +13 -0
  195. package/dist/ports/renderer.d.ts +23 -0
  196. package/dist/ports/renderer.js +7 -0
  197. package/dist/ports/resource-context.d.ts +51 -0
  198. package/dist/ports/resource-context.js +9 -0
  199. package/dist/ports/scm.d.ts +40 -0
  200. package/dist/ports/scm.js +6 -0
  201. package/dist/ports/state-store.d.ts +97 -0
  202. package/dist/ports/state-store.js +12 -0
  203. package/dist/presets/balanced.json +8 -0
  204. package/dist/presets/conservative.json +9 -0
  205. package/dist/presets/lightweight.json +8 -0
  206. package/dist/profiles/example-team/profile.json +101 -0
  207. package/dist/profiles/pilot-local/profile.json +56 -0
  208. package/dist/schemas/profile.d.ts +631 -0
  209. package/dist/schemas/profile.js +287 -0
  210. package/package.json +55 -0
@@ -0,0 +1,170 @@
1
+ // GitHub Context Adapters — Inventory · ResourceContext · ChangeContext (C-09 §2).
2
+ //
3
+ // 세 Port를 한 파일에 두는 이유는 같은 client와 같은 참조 문법을 쓰기 때문이다.
4
+ // 인터페이스는 셋으로 나뉘어 있으므로 소비자는 필요한 것만 받는다.
5
+ //
6
+ // revisionMarker는 여기서 만든다 — 무엇이 실질 변화인지는 provider가 아는 사실이고,
7
+ // Core는 그 값을 비교만 한다 (C-07 §4.2).
8
+ import { parseThreadRef } from "./scm.js";
9
+ const labelNames = (labels) => (labels ?? []).map((l) => (typeof l === 'string' ? l : (l.name ?? ''))).filter((n) => n.length > 0);
10
+ /**
11
+ * 실질 변화 마커. 갱신 시각 하나로는 댓글 외의 변화를 놓치므로 여러 조각을 묶는다
12
+ * (C-07 §4.2 — 무엇을 넣을지는 adapter가 정한다).
13
+ */
14
+ const marker = (parts) => parts.map((p) => (p === undefined ? '' : String(p))).join('|');
15
+ class GitHubContextBase {
16
+ id = 'github';
17
+ client;
18
+ defaultRepo;
19
+ constructor(deps) {
20
+ this.client = deps.client;
21
+ this.defaultRepo = deps.defaultRepo;
22
+ }
23
+ expand(reference) {
24
+ const trimmed = reference.trim();
25
+ return trimmed.startsWith('#') && this.defaultRepo ? `${this.defaultRepo}${trimmed}` : trimmed;
26
+ }
27
+ }
28
+ /**
29
+ * 상태 무관 열거 (C-09 §2). `state=all`이 이 Port의 존재 이유다 —
30
+ * 닫힌 것을 빼면 닫힌 뒤에 일어난 일을 통째로 놓친다.
31
+ *
32
+ * GitHub은 PR도 issues 목록에 실어 주므로 한 번의 열거로 둘 다 덮인다.
33
+ * 그래서 `kinds`를 받아도 별도 호출로 나누지 않고 결과에서 가른다.
34
+ */
35
+ export class GitHubInventory extends GitHubContextBase {
36
+ #pageSize;
37
+ constructor(deps) {
38
+ super(deps);
39
+ this.#pageSize = deps.pageSize ?? 100;
40
+ }
41
+ async enumerate(query, cursor) {
42
+ if (!this.defaultRepo) {
43
+ // 어디를 열거할지 모른다. 빈 목록을 "없다"로 돌려주면 census가 전부 사라졌다고 본다.
44
+ return { items: [], complete: false };
45
+ }
46
+ const page = cursor ? Number(cursor) : 1;
47
+ const params = new URLSearchParams({
48
+ state: 'all',
49
+ per_page: String(this.#pageSize),
50
+ page: String(page),
51
+ sort: 'updated',
52
+ direction: 'desc',
53
+ });
54
+ if (query.updatedSince)
55
+ params.set('since', query.updatedSince);
56
+ const response = await this.client.get(`/repos/${this.defaultRepo}/issues?${params}`);
57
+ // 못 읽었으면 complete=false다. 페이지를 다 돌지 못했는데 끝인 척하면
58
+ // Census가 "사라졌다"를 잘못 만들어낸다 (C-07 §1.5).
59
+ if (!response.ok || !response.data)
60
+ return { items: [], complete: false };
61
+ const wanted = new Set(query.kinds ?? []);
62
+ const items = [];
63
+ for (const issue of response.data) {
64
+ const kind = issue.pull_request ? 'change' : 'issue';
65
+ if (wanted.size > 0 && !wanted.has(kind))
66
+ continue;
67
+ items.push({
68
+ reference: `${this.defaultRepo}#${issue.number}`,
69
+ state: issue.state,
70
+ updatedAt: issue.updated_at,
71
+ revisionMarker: marker([issue.updated_at, issue.comments, issue.state]),
72
+ title: issue.title,
73
+ assignees: (issue.assignees ?? []).map((a) => a.login),
74
+ labels: labelNames(issue.labels),
75
+ });
76
+ }
77
+ const full = response.data.length === this.#pageSize;
78
+ return {
79
+ items,
80
+ ...(full ? { next: String(page + 1) } : {}),
81
+ // 마지막 페이지에서만 완주를 말한다 (ports/inventory.ts 계약).
82
+ complete: !full,
83
+ };
84
+ }
85
+ }
86
+ export class GitHubResourceContext extends GitHubContextBase {
87
+ async getResource(reference) {
88
+ const ref = parseThreadRef(this.expand(reference));
89
+ if (!ref)
90
+ return missingResource(reference);
91
+ const response = await this.client.get(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}`);
92
+ if (!response.ok || !response.data)
93
+ return missingResource(reference);
94
+ const issue = response.data;
95
+ return {
96
+ reference,
97
+ state: issue.state,
98
+ title: issue.title,
99
+ ...(issue.body ? { body: issue.body } : {}),
100
+ ...(issue.user?.login ? { author: issue.user.login } : {}),
101
+ assignees: (issue.assignees ?? []).map((a) => a.login),
102
+ labels: labelNames(issue.labels),
103
+ updatedAt: issue.updated_at,
104
+ revisionMarker: marker([issue.updated_at, issue.comments, issue.state]),
105
+ };
106
+ }
107
+ /**
108
+ * 최근 것부터. 전문을 통째로 넘기지 않는 것이 이 Port의 계약이므로 개수를 호출자가 정한다
109
+ * — 조사 depth가 예산을 쥔다 (C-05 §3).
110
+ */
111
+ async getComments(reference, query = {}) {
112
+ const ref = parseThreadRef(this.expand(reference));
113
+ if (!ref)
114
+ return [];
115
+ const params = new URLSearchParams({ per_page: String(query.limit ?? 20), sort: 'created', direction: 'desc' });
116
+ if (query.since)
117
+ params.set('since', query.since);
118
+ const response = await this.client.get(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/comments?${params}`);
119
+ if (!response.ok || !response.data)
120
+ return [];
121
+ return response.data.map((comment) => ({
122
+ id: String(comment.id),
123
+ author: comment.user?.login ?? '(unknown)',
124
+ at: comment.updated_at ?? comment.created_at,
125
+ body: comment.body ?? '',
126
+ // unresolved 여부를 이 API가 말해주지 않는다. 모르는 것을 false로 적지 않는다.
127
+ }));
128
+ }
129
+ }
130
+ export class GitHubChangeContext extends GitHubContextBase {
131
+ #pageSize;
132
+ constructor(deps) {
133
+ super(deps);
134
+ this.#pageSize = deps.pageSize ?? 100;
135
+ }
136
+ async getChange(reference) {
137
+ const ref = parseThreadRef(this.expand(reference));
138
+ if (!ref)
139
+ return { reference, changedPaths: [], revisionMarker: '', missing: true };
140
+ const base = `/repos/${ref.owner}/${ref.repo}/pulls/${ref.number}`;
141
+ const pull = await this.client.get(base);
142
+ if (!pull.ok || !pull.data) {
143
+ // 변경요청이 아니거나 못 읽었다. 둘 다 "경로가 없다"가 아니라 "모른다"이다.
144
+ return { reference, changedPaths: [], revisionMarker: '', missing: true };
145
+ }
146
+ const files = await this.client.get(`${base}/files?per_page=${this.#pageSize}`);
147
+ const paths = (files.data ?? []).map((f) => f.filename);
148
+ // 한 페이지를 꽉 채웠으면 뒤가 더 있을 수 있다. 그 사실을 숨기면
149
+ // "내 영역은 안 바뀌었다"는 틀린 판정이 나온다.
150
+ const truncated = paths.length === this.#pageSize || !files.ok;
151
+ const reviews = await this.client.get(`${base}/reviews?per_page=20`);
152
+ const reviewState = (reviews.data ?? []).at(-1)?.state;
153
+ return {
154
+ reference,
155
+ changedPaths: paths,
156
+ ...(truncated ? { truncated: true } : {}),
157
+ ...(pull.data.head?.sha ? { revisions: [pull.data.head.sha] } : {}),
158
+ revisionMarker: marker([pull.data.updated_at, pull.data.head?.sha, pull.data.state, reviewState]),
159
+ ...(reviewState ? { reviewState } : {}),
160
+ };
161
+ }
162
+ }
163
+ const missingResource = (reference) => ({
164
+ reference,
165
+ state: 'unknown',
166
+ title: '',
167
+ updatedAt: '',
168
+ revisionMarker: '',
169
+ missing: true,
170
+ });
@@ -0,0 +1,55 @@
1
+ import type { Cursor, EventBatch, EventSource } from '../../ports/event-source.ts';
2
+ import type { GitHubClient } from './client.ts';
3
+ /**
4
+ * cursor에 담기는 것. 엔드포인트마다 기준선이 달라 한 값으로 못 묶는다.
5
+ * 문자열로 직렬화해 Port 계약(Cursor = string | null)에 맞춘다.
6
+ *
7
+ * `since`는 경계를 포함하므로 마지막 항목이 다음 회차에 다시 온다. 그대로 둔다 —
8
+ * 1초를 밀어 중복을 없애면 같은 시각에 달린 다른 댓글이 통째로 사라진다.
9
+ * 누락보다 중복이 안전하고, 중복은 event key exact lookup이 거른다 (OM §10.5).
10
+ *
11
+ * `page`는 한 배치가 perPage를 꽉 채웠을 때만 세워진다. 같은 시각 이벤트가 한 페이지를
12
+ * 넘겨도 다음 회차가 이어서 받도록 하는 장치다.
13
+ */
14
+ export type GitHubCursor = {
15
+ notificationsSince?: string;
16
+ notificationsLastModified?: string;
17
+ notificationsPage?: number;
18
+ commentsSince?: string;
19
+ commentsPage?: number;
20
+ reviewCommentsSince?: string;
21
+ reviewCommentsPage?: number;
22
+ };
23
+ export declare function parseCursor(cursor: Cursor): GitHubCursor;
24
+ export type GitHubEventSourceDeps = {
25
+ client: GitHubClient;
26
+ /** `owner/repo`. 알림도 댓글도 이 저장소 것만 본다. */
27
+ repo: string;
28
+ /** 한 번에 가져올 최대 개수. 폭주해도 한 배치가 감당 가능한 크기로 끊는다. */
29
+ perPage?: number;
30
+ };
31
+ export declare class GitHubEventSource implements EventSource {
32
+ #private;
33
+ readonly id = "github-poll";
34
+ constructor(deps: GitHubEventSourceDeps);
35
+ /** 세 갈래 모두 같은 시점부터 본다. 페이지 상태는 비워 둔다 — 아직 아무것도 읽지 않았다. */
36
+ cursorFrom(since: string): Cursor;
37
+ drain(cursor: Cursor): Promise<EventBatch>;
38
+ }
39
+ /**
40
+ * GitHub notification의 `reason` 어휘를 Generic Signal로 옮기는 매핑.
41
+ *
42
+ * Core에 두지 않는 이유는 이것이 GitHub의 말이기 때문이다. `mention`·`review_requested`는
43
+ * 이 provider가 쓰는 단어이고, 다른 곳은 다른 단어를 쓴다. Adapter가 자기 어휘를 내놓고
44
+ * Profile이 그것을 고르면, Core는 끝까지 신호의 이름만 알면 된다 (OM §10.6).
45
+ */
46
+ export declare const GITHUB_REASON_SIGNALS: {
47
+ readonly assign: "assigned_to_me";
48
+ readonly mention: "mentioned_me";
49
+ readonly team_mention: "mentioned_me";
50
+ readonly review_requested: "review_requested";
51
+ readonly author: "my_pr_reviewed";
52
+ readonly comment: "participated_thread_changed";
53
+ readonly subscribed: "participated_thread_changed";
54
+ readonly state_change: "participated_thread_changed";
55
+ };
@@ -0,0 +1,183 @@
1
+ // GitHub polling Event Source.
2
+ //
3
+ // 하는 일은 "무슨 일이 있었는지" 모아 오는 것까지다. 무엇이 중요한지, 누구에게 알릴지는
4
+ // Monitor가 정한다 (B-09) — 여기서 우선순위를 매기기 시작하면 Profile 없이 프로젝트
5
+ // 사정을 아는 Adapter가 되어버린다.
6
+ //
7
+ // cursor는 호출자가 갱신한다. Phase B가 중간에 실패했을 때 cursor를 전진시키지 않고
8
+ // 다시 받는 편이 안전하기 때문이다 — 누락보다 중복이 낫고, 중복은 event key가 거른다
9
+ // (OM §10.5).
10
+ /**
11
+ * 다음 조회를 어디서부터 시작할지. 마지막 항목보다 조금 **뒤로** 잡는다.
12
+ *
13
+ * `since`는 그 시각 이후를 주므로, 마지막 항목의 시각을 그대로 쓰면 그 직후 같은 초에
14
+ * 달린 이벤트가 영영 안 온다. 앞으로 미는 것(bump)은 더 나쁘고, 정확히 맞추는 것도
15
+ * 초 단위 경계에서는 불가능하다. 그래서 조금 겹쳐 읽고 중복은 key로 거른다.
16
+ */
17
+ const OVERLAP_MS = 1000;
18
+ function watermark(timestamp) {
19
+ return new Date(new Date(timestamp).getTime() - OVERLAP_MS).toISOString();
20
+ }
21
+ /**
22
+ * 한 페이지를 읽고 나서 기준선과 페이지를 어떻게 옮길지 정한다.
23
+ * 꽉 찬 페이지는 뒤가 더 있다는 뜻이므로 기준선을 건드리지 않고 페이지만 넘긴다 —
24
+ * 같은 시각 이벤트가 페이지 경계에 걸려도 잃지 않는다.
25
+ */
26
+ function advance(rowCount, perPage, since, page, lastTimestamp) {
27
+ if (rowCount >= perPage)
28
+ return { ...(since !== undefined ? { since } : {}), page: page + 1 };
29
+ if (lastTimestamp !== undefined)
30
+ return { since: watermark(lastTimestamp) };
31
+ return since !== undefined ? { since } : {};
32
+ }
33
+ export function parseCursor(cursor) {
34
+ if (!cursor)
35
+ return {};
36
+ try {
37
+ return JSON.parse(cursor);
38
+ }
39
+ catch {
40
+ return {};
41
+ }
42
+ }
43
+ export class GitHubEventSource {
44
+ id = 'github-poll';
45
+ #client;
46
+ #repo;
47
+ #perPage;
48
+ constructor(deps) {
49
+ this.#client = deps.client;
50
+ this.#repo = deps.repo;
51
+ this.#perPage = deps.perPage ?? 50;
52
+ }
53
+ /** 세 갈래 모두 같은 시점부터 본다. 페이지 상태는 비워 둔다 — 아직 아무것도 읽지 않았다. */
54
+ cursorFrom(since) {
55
+ return JSON.stringify({ notificationsSince: since, commentsSince: since, reviewCommentsSince: since });
56
+ }
57
+ async drain(cursor) {
58
+ const state = parseCursor(cursor);
59
+ const events = [];
60
+ const next = { ...state };
61
+ // 1. 알림 — mention·assign·review_requested가 여기로 온다.
62
+ // 전역이 아니라 저장소 한정으로 묻는다. `.asc/`는 프로젝트마다 따로 있으므로
63
+ // 다른 저장소의 알림이 이 Runtime에 섞이면 안 된다 (OM §3.2).
64
+ // all=true인 이유는 감지가 사람의 읽음 처리에 좌우되면 안 되기 때문이다 —
65
+ // GitHub 웹에서 먼저 읽었다는 이유로 Monitor가 그 사건을 놓치면 안 된다.
66
+ const notificationPage = state.notificationsPage ?? 1;
67
+ const notifications = await this.#client.get(`/repos/${this.#repo}/notifications?all=true&per_page=${this.#perPage}&page=${notificationPage}` +
68
+ (state.notificationsSince ? `&since=${state.notificationsSince}` : ''),
69
+ // 페이지를 이어받는 중이면 조건부 요청을 쓰지 않는다. 304가 오면 남은 페이지를
70
+ // 영영 못 받기 때문이다.
71
+ state.notificationsLastModified && state.notificationsPage === undefined
72
+ ? { ifModifiedSince: state.notificationsLastModified }
73
+ : {});
74
+ if (notifications.ok && notifications.data) {
75
+ for (const item of notifications.data) {
76
+ events.push({
77
+ eventKey: `notification:${item.id}:${item.updated_at}`,
78
+ detectedAt: item.updated_at,
79
+ reference: referenceOf(item),
80
+ hints: { actors: [], labels: [] },
81
+ raw: { kind: 'notification', reason: item.reason, subjectType: item.subject.type, title: item.subject.title },
82
+ });
83
+ }
84
+ const rows = notifications.data;
85
+ const moved = advance(rows.length, this.#perPage, state.notificationsSince, notificationPage, rows.at(-1)?.updated_at);
86
+ if (moved.since !== undefined)
87
+ next.notificationsSince = moved.since;
88
+ if (moved.page === undefined)
89
+ delete next.notificationsPage;
90
+ else
91
+ next.notificationsPage = moved.page;
92
+ // 마지막 페이지까지 읽었을 때만 조건부 요청 기준을 갱신한다
93
+ if (notifications.lastModified && moved.page === undefined) {
94
+ next.notificationsLastModified = notifications.lastModified;
95
+ }
96
+ }
97
+ // 2. 이슈 댓글 — 알림이 오지 않는 참여 스레드의 움직임까지 본다
98
+ const comments = await this.#drainComments('issues', state.commentsSince, state.commentsPage);
99
+ for (const comment of comments.rows) {
100
+ events.push({
101
+ eventKey: `comment:${comment.id}`,
102
+ detectedAt: comment.updated_at,
103
+ reference: issueRefOf(comment, this.#repo),
104
+ hints: { actors: comment.user ? [comment.user.login] : [] },
105
+ raw: { kind: 'issue_comment', url: comment.html_url, body: comment.body },
106
+ });
107
+ }
108
+ if (comments.read) {
109
+ if (comments.since !== undefined)
110
+ next.commentsSince = comments.since;
111
+ if (comments.page === undefined)
112
+ delete next.commentsPage;
113
+ else
114
+ next.commentsPage = comments.page;
115
+ }
116
+ // 3. PR 리뷰 댓글 — 코드 줄에 달린 것들
117
+ const reviewComments = await this.#drainComments('pulls', state.reviewCommentsSince, state.reviewCommentsPage);
118
+ for (const comment of reviewComments.rows) {
119
+ events.push({
120
+ eventKey: `review_comment:${comment.id}`,
121
+ detectedAt: comment.updated_at,
122
+ reference: pullRefOf(comment, this.#repo),
123
+ hints: { actors: comment.user ? [comment.user.login] : [] },
124
+ raw: { kind: 'review_comment', url: comment.html_url, body: comment.body },
125
+ });
126
+ }
127
+ if (reviewComments.read) {
128
+ if (reviewComments.since !== undefined)
129
+ next.reviewCommentsSince = reviewComments.since;
130
+ if (reviewComments.page === undefined)
131
+ delete next.reviewCommentsPage;
132
+ else
133
+ next.reviewCommentsPage = reviewComments.page;
134
+ }
135
+ // review 제출(승인/변경요청) 자체는 여기서 긁지 않는다. 본인이 관련된 것은 알림으로
136
+ // 들어오고, 그 밖의 것까지 보려면 열린 PR을 전부 순회해야 해서 값이 비싸다.
137
+ // 필요해지면 `/repos/{repo}/pulls/{n}/reviews`로 `review:<id>` key를 만든다.
138
+ events.sort((a, b) => a.detectedAt.localeCompare(b.detectedAt));
139
+ const hasMore = next.notificationsPage !== undefined ||
140
+ next.commentsPage !== undefined ||
141
+ next.reviewCommentsPage !== undefined;
142
+ return { events, cursor: JSON.stringify(next), hasMore };
143
+ }
144
+ /** 댓글 한 페이지를 받고 기준선을 옮긴다 (`advance` 참조). */
145
+ async #drainComments(kind, since, page) {
146
+ const current = page ?? 1;
147
+ const response = await this.#client.get(`/repos/${this.#repo}/${kind}/comments?per_page=${this.#perPage}&sort=updated&direction=asc&page=${current}` +
148
+ (since ? `&since=${since}` : ''));
149
+ if (!response.ok || !response.data)
150
+ return { read: false, rows: [] };
151
+ const rows = response.data;
152
+ return { read: true, rows, ...advance(rows.length, this.#perPage, since, current, rows.at(-1)?.updated_at) };
153
+ }
154
+ }
155
+ function referenceOf(item) {
156
+ const number = item.subject.url?.match(/\/(\d+)$/)?.[1];
157
+ return number ? `${item.repository.full_name}#${number}` : item.repository.full_name;
158
+ }
159
+ function issueRefOf(comment, repo) {
160
+ const number = comment.issue_url?.match(/\/(\d+)$/)?.[1];
161
+ return number ? `${repo}#${number}` : repo;
162
+ }
163
+ function pullRefOf(comment, repo) {
164
+ const number = comment.pull_request_url?.match(/\/(\d+)$/)?.[1];
165
+ return number ? `${repo}#${number}` : repo;
166
+ }
167
+ /**
168
+ * GitHub notification의 `reason` 어휘를 Generic Signal로 옮기는 매핑.
169
+ *
170
+ * Core에 두지 않는 이유는 이것이 GitHub의 말이기 때문이다. `mention`·`review_requested`는
171
+ * 이 provider가 쓰는 단어이고, 다른 곳은 다른 단어를 쓴다. Adapter가 자기 어휘를 내놓고
172
+ * Profile이 그것을 고르면, Core는 끝까지 신호의 이름만 알면 된다 (OM §10.6).
173
+ */
174
+ export const GITHUB_REASON_SIGNALS = {
175
+ assign: 'assigned_to_me',
176
+ mention: 'mentioned_me',
177
+ team_mention: 'mentioned_me',
178
+ review_requested: 'review_requested',
179
+ author: 'my_pr_reviewed',
180
+ comment: 'participated_thread_changed',
181
+ subscribed: 'participated_thread_changed',
182
+ state_change: 'participated_thread_changed',
183
+ };
@@ -0,0 +1,50 @@
1
+ import type { CanonicalSnapshot } from '../../core/model/entities.ts';
2
+ import type { BaselineQuery, ExternalAction, ExternalActionResult, ScmPort, ThreadSnapshot } from '../../ports/scm.ts';
3
+ import type { GitHubClient } from './client.ts';
4
+ /** `owner/repo#19` 를 쪼갠다. 다른 형태는 다루지 않는다. */
5
+ export declare function parseThreadRef(reference: string): {
6
+ owner: string;
7
+ repo: string;
8
+ number: number;
9
+ } | null;
10
+ export type GitHubScmDeps = {
11
+ client: GitHubClient;
12
+ /** `owner/repo`. 짧은 참조(`#19`)를 풀 때 쓴다. */
13
+ defaultRepo?: string;
14
+ /** canonical source id → git ref. Profile이 채운다 (OM §8). */
15
+ sourceRefs?: Readonly<Record<string, {
16
+ owner?: string;
17
+ repo?: string;
18
+ ref: string;
19
+ }>>;
20
+ };
21
+ export declare class GitHubScm implements ScmPort {
22
+ #private;
23
+ readonly id = "github";
24
+ constructor(deps: GitHubScmDeps);
25
+ /**
26
+ * 스레드의 현재 상태를 하나의 표식으로 만든다. Drift Guard가 이 값으로 "승인 이후 뭔가
27
+ * 달라졌는가"를 판단하므로, 조회가 실패하면 변화 없음으로 넘기지 않고 missing으로
28
+ * 돌려준다 — 모르면 실행하지 않는 쪽이 맞다 (OM §11.9).
29
+ *
30
+ * 표식은 세 조각을 묶는다:
31
+ * issue의 updated_at — 제목·본문 수정을 잡는다
32
+ * 댓글 수 — 추가·삭제를 잡는다
33
+ * 마지막 댓글의 id·updated_at — 새 댓글과 그 수정을 잡는다
34
+ *
35
+ * 마지막 댓글은 `per_page=1&page=<댓글 수>`로 직접 집는다. 첫 page만 보면 댓글이 100개를
36
+ * 넘는 순간 새 댓글을 못 보고 오래된 초안이 그대로 나간다.
37
+ *
38
+ * ponytail: 중간 댓글 하나만 수정된 경우는 이 표식으로 잡히지 않는다. 전부 잡으려면
39
+ * 모든 댓글을 훑어 해시를 내야 하는데 스레드마다 매번 그러기엔 비싸다. 필요해지면
40
+ * 마지막 N개만 해시하는 쪽으로 좁혀서 올린다.
41
+ */
42
+ getThread(reference: string): Promise<ThreadSnapshot>;
43
+ /** source별 현재 baseline. 하나라도 못 읽으면 그 source는 `unknown`으로 남는다. */
44
+ getBaselines(queries: readonly BaselineQuery[]): Promise<CanonicalSnapshot[]>;
45
+ /**
46
+ * 승인된 단일 행위. 아는 action만 수행하며, 그 외에는 아무것도 하지 않는다 —
47
+ * Grant의 allowedWrites 검사(Executor)에 더해 Adapter도 자기 몫으로 닫아 둔다.
48
+ */
49
+ execute(action: ExternalAction): Promise<ExternalActionResult>;
50
+ }
@@ -0,0 +1,104 @@
1
+ // GitHub SCM Adapter — 스레드·정본 조회와, 승인된 단일 행위의 실행.
2
+ //
3
+ // 쓰기는 `execute` 하나뿐이고, 그 함수는 Grant를 쥔 Executor만 부른다 (OM §11.5).
4
+ // Adapter는 권한을 판단하지 않는다 — 판단은 이미 끝났고 여기는 실행만 한다.
5
+ // 대신 Grant가 지정한 action 밖의 일은 하지 않는다: 아는 행위가 아니면 거절한다.
6
+ /** `owner/repo#19` 를 쪼갠다. 다른 형태는 다루지 않는다. */
7
+ export function parseThreadRef(reference) {
8
+ const match = /^([^/\s]+)\/([^#\s]+)#(\d+)$/.exec(reference.trim());
9
+ if (!match)
10
+ return null;
11
+ return { owner: match[1], repo: match[2], number: Number(match[3]) };
12
+ }
13
+ export class GitHubScm {
14
+ id = 'github';
15
+ #client;
16
+ #defaultRepo;
17
+ #sourceRefs;
18
+ constructor(deps) {
19
+ this.#client = deps.client;
20
+ this.#defaultRepo = deps.defaultRepo;
21
+ this.#sourceRefs = deps.sourceRefs ?? {};
22
+ }
23
+ /**
24
+ * 스레드의 현재 상태를 하나의 표식으로 만든다. Drift Guard가 이 값으로 "승인 이후 뭔가
25
+ * 달라졌는가"를 판단하므로, 조회가 실패하면 변화 없음으로 넘기지 않고 missing으로
26
+ * 돌려준다 — 모르면 실행하지 않는 쪽이 맞다 (OM §11.9).
27
+ *
28
+ * 표식은 세 조각을 묶는다:
29
+ * issue의 updated_at — 제목·본문 수정을 잡는다
30
+ * 댓글 수 — 추가·삭제를 잡는다
31
+ * 마지막 댓글의 id·updated_at — 새 댓글과 그 수정을 잡는다
32
+ *
33
+ * 마지막 댓글은 `per_page=1&page=<댓글 수>`로 직접 집는다. 첫 page만 보면 댓글이 100개를
34
+ * 넘는 순간 새 댓글을 못 보고 오래된 초안이 그대로 나간다.
35
+ *
36
+ * ponytail: 중간 댓글 하나만 수정된 경우는 이 표식으로 잡히지 않는다. 전부 잡으려면
37
+ * 모든 댓글을 훑어 해시를 내야 하는데 스레드마다 매번 그러기엔 비싸다. 필요해지면
38
+ * 마지막 N개만 해시하는 쪽으로 좁혀서 올린다.
39
+ */
40
+ async getThread(reference) {
41
+ const ref = parseThreadRef(this.#expand(reference));
42
+ if (!ref)
43
+ return { reference, lastEventId: '', missing: true };
44
+ const base = `/repos/${ref.owner}/${ref.repo}/issues/${ref.number}`;
45
+ const issue = await this.#client.get(base);
46
+ if (!issue.ok || !issue.data)
47
+ return { reference, lastEventId: '', missing: true };
48
+ const count = issue.data.comments;
49
+ const marker = `issue:${issue.data.updated_at}|c${count}`;
50
+ if (count === 0)
51
+ return { reference, lastEventId: marker };
52
+ const last = await this.#client.get(`${base}/comments?per_page=1&page=${count}`);
53
+ if (!last.ok || !last.data) {
54
+ // 댓글이 있다는데 못 읽었다. 안다고 답할 수 없으므로 실행을 막는다.
55
+ return { reference, lastEventId: '', missing: true };
56
+ }
57
+ const tail = last.data.at(-1);
58
+ return {
59
+ reference,
60
+ lastEventId: tail ? `${marker}|${tail.id}:${tail.updated_at}` : marker,
61
+ };
62
+ }
63
+ /** source별 현재 baseline. 하나라도 못 읽으면 그 source는 `unknown`으로 남는다. */
64
+ async getBaselines(queries) {
65
+ const out = [];
66
+ for (const query of queries) {
67
+ const configured = this.#sourceRefs[query.sourceId];
68
+ const ref = query.ref ?? configured?.ref;
69
+ const repo = this.#repoOf(configured);
70
+ if (!ref || !repo) {
71
+ out.push({ sourceId: query.sourceId, baseline: 'unknown' });
72
+ continue;
73
+ }
74
+ const response = await this.#client.get(`/repos/${repo}/commits/${encodeURIComponent(ref)}`);
75
+ const sha = response.data?.sha ?? response.data?.object?.sha;
76
+ out.push({ sourceId: query.sourceId, baseline: sha ?? 'unknown' });
77
+ }
78
+ return out;
79
+ }
80
+ /**
81
+ * 승인된 단일 행위. 아는 action만 수행하며, 그 외에는 아무것도 하지 않는다 —
82
+ * Grant의 allowedWrites 검사(Executor)에 더해 Adapter도 자기 몫으로 닫아 둔다.
83
+ */
84
+ async execute(action) {
85
+ if (action.action !== 'github.issue_comment.create') {
86
+ return { ok: false, error: `unsupported action: ${action.action}` };
87
+ }
88
+ const ref = parseThreadRef(this.#expand(action.target));
89
+ if (!ref)
90
+ return { ok: false, error: `unrecognized target: ${action.target}` };
91
+ const response = await this.#client.post(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/comments`, { body: action.payload });
92
+ if (!response.ok || !response.data)
93
+ return { ok: false, error: response.error ?? `HTTP ${response.status}` };
94
+ return { ok: true, resultRef: response.data.html_url };
95
+ }
96
+ #expand(reference) {
97
+ return reference.startsWith('#') && this.#defaultRepo ? `${this.#defaultRepo}${reference}` : reference;
98
+ }
99
+ #repoOf(configured) {
100
+ if (configured?.owner && configured.repo)
101
+ return `${configured.owner}/${configured.repo}`;
102
+ return this.#defaultRepo ?? null;
103
+ }
104
+ }
@@ -0,0 +1,36 @@
1
+ import type { AdapterDescriptor, BindingCandidate } from '../../core/binding/types.ts';
2
+ import type { Adapter, DiscoveryContext, ProbeResult } from '../../ports/adapter.ts';
3
+ export type GitLabAdapterDeps = {
4
+ listRemotes?: (projectRoot: string) => Promise<string[]>;
5
+ findToken?: (env?: NodeJS.ProcessEnv) => string | null;
6
+ reach?: (project: string, token: string) => Promise<{
7
+ ok: boolean;
8
+ detail?: string;
9
+ }>;
10
+ /**
11
+ * 명시 override. **필수가 아니다** — 기본은 remote에서 host를 읽는다.
12
+ * 자체 호스팅 주소를 코드나 문서에 박지 않기 위해서다 (지시 §12).
13
+ */
14
+ host?: string;
15
+ };
16
+ export declare class GitLabAdapter implements Adapter {
17
+ #private;
18
+ constructor(deps?: GitLabAdapterDeps);
19
+ describe(): AdapterDescriptor;
20
+ /**
21
+ * remote **전부**를 본다. `origin` 하나만 보면 origin이 코드 정본이 아닌 프로젝트에서
22
+ * 아무것도 못 찾는다 (지시 §13).
23
+ *
24
+ * host를 미리 알고 있지 않다 — 자체 호스팅이 흔하고, 주소를 코드에 박으면 그 프로젝트
25
+ * 하나를 위한 adapter가 된다. 대신 **다른 adapter가 맡는 host만 비켜** 나머지를 후보로
26
+ * 올린다. 후보는 추측이 아니라 후보이며, 실제로 되는지는 probe가 정한다.
27
+ */
28
+ discover(context: DiscoveryContext): Promise<BindingCandidate[]>;
29
+ probe(candidate: BindingCandidate, context: DiscoveryContext): Promise<ProbeResult>;
30
+ /** 이 후보가 어느 주소를 가리키는지. composition이 Port를 만들 때 같은 값을 써야 한다. */
31
+ endpointFor(resource: string): string | undefined;
32
+ }
33
+ export declare function parseRemote(url: string): {
34
+ host: string;
35
+ project: string;
36
+ } | null;