@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.
- package/LICENSE +15 -0
- package/README.md +22 -0
- package/dist/adapters/claude-code/binding.d.ts +5 -0
- package/dist/adapters/claude-code/binding.js +14 -0
- package/dist/adapters/claude-code/guard.d.ts +54 -0
- package/dist/adapters/claude-code/guard.js +295 -0
- package/dist/adapters/claude-code/install.d.ts +67 -0
- package/dist/adapters/claude-code/install.js +231 -0
- package/dist/adapters/claude-code/observer.d.ts +38 -0
- package/dist/adapters/claude-code/observer.js +77 -0
- package/dist/adapters/claude-code/probe.d.ts +53 -0
- package/dist/adapters/claude-code/probe.js +129 -0
- package/dist/adapters/claude-code/skill.d.ts +8 -0
- package/dist/adapters/claude-code/skill.js +267 -0
- package/dist/adapters/fixture-work/index.d.ts +34 -0
- package/dist/adapters/fixture-work/index.js +78 -0
- package/dist/adapters/github/adapter.d.ts +32 -0
- package/dist/adapters/github/adapter.js +95 -0
- package/dist/adapters/github/client.d.ts +30 -0
- package/dist/adapters/github/client.js +90 -0
- package/dist/adapters/github/context.d.ts +44 -0
- package/dist/adapters/github/context.js +170 -0
- package/dist/adapters/github/event-source.d.ts +55 -0
- package/dist/adapters/github/event-source.js +183 -0
- package/dist/adapters/github/scm.d.ts +50 -0
- package/dist/adapters/github/scm.js +104 -0
- package/dist/adapters/gitlab/adapter.d.ts +36 -0
- package/dist/adapters/gitlab/adapter.js +117 -0
- package/dist/adapters/gitlab/client.d.ts +30 -0
- package/dist/adapters/gitlab/client.js +47 -0
- package/dist/adapters/gitlab/ports.d.ts +40 -0
- package/dist/adapters/gitlab/ports.js +186 -0
- package/dist/adapters/jam/adapter.d.ts +53 -0
- package/dist/adapters/jam/adapter.js +144 -0
- package/dist/adapters/jam/event-source.d.ts +28 -0
- package/dist/adapters/jam/event-source.js +65 -0
- package/dist/adapters/jam/mcp-client.d.ts +50 -0
- package/dist/adapters/jam/mcp-client.js +218 -0
- package/dist/adapters/jam/ports.d.ts +51 -0
- package/dist/adapters/jam/ports.js +167 -0
- package/dist/adapters/local/identity.d.ts +27 -0
- package/dist/adapters/local/identity.js +40 -0
- package/dist/adapters/local/presentation.d.ts +14 -0
- package/dist/adapters/local/presentation.js +42 -0
- package/dist/adapters/markdown/layout.d.ts +15 -0
- package/dist/adapters/markdown/layout.js +49 -0
- package/dist/adapters/markdown/serialize.d.ts +9 -0
- package/dist/adapters/markdown/serialize.js +120 -0
- package/dist/adapters/markdown/state-store.d.ts +23 -0
- package/dist/adapters/markdown/state-store.js +316 -0
- package/dist/adapters/mattermost/client.d.ts +23 -0
- package/dist/adapters/mattermost/client.js +54 -0
- package/dist/adapters/mattermost/presentation.d.ts +43 -0
- package/dist/adapters/mattermost/presentation.js +88 -0
- package/dist/adapters/memory/mocks.d.ts +87 -0
- package/dist/adapters/memory/mocks.js +163 -0
- package/dist/adapters/memory/runtime-binding.d.ts +23 -0
- package/dist/adapters/memory/runtime-binding.js +118 -0
- package/dist/adapters/memory/state-store.d.ts +17 -0
- package/dist/adapters/memory/state-store.js +125 -0
- package/dist/adapters/text/renderer.d.ts +7 -0
- package/dist/adapters/text/renderer.js +81 -0
- package/dist/adapters/webhook/ingress.d.ts +103 -0
- package/dist/adapters/webhook/ingress.js +150 -0
- package/dist/cli/asc.d.ts +16 -0
- package/dist/cli/asc.js +2914 -0
- package/dist/cli/identity-config.d.ts +7 -0
- package/dist/cli/identity-config.js +31 -0
- package/dist/composition/observe.d.ts +21 -0
- package/dist/composition/observe.js +72 -0
- package/dist/composition/registry.d.ts +33 -0
- package/dist/composition/registry.js +62 -0
- package/dist/composition/runtime.d.ts +63 -0
- package/dist/composition/runtime.js +155 -0
- package/dist/core/approval/service.d.ts +17 -0
- package/dist/core/approval/service.js +129 -0
- package/dist/core/attach/bootstrap.d.ts +102 -0
- package/dist/core/attach/bootstrap.js +178 -0
- package/dist/core/attach/init.d.ts +29 -0
- package/dist/core/attach/init.js +100 -0
- package/dist/core/attach/setup-plan.d.ts +125 -0
- package/dist/core/attach/setup-plan.js +177 -0
- package/dist/core/attach/setup.d.ts +40 -0
- package/dist/core/attach/setup.js +140 -0
- package/dist/core/binding/types.d.ts +93 -0
- package/dist/core/binding/types.js +74 -0
- package/dist/core/distribution/release.d.ts +15 -0
- package/dist/core/distribution/release.js +27 -0
- package/dist/core/distribution/runtime-install.d.ts +50 -0
- package/dist/core/distribution/runtime-install.js +90 -0
- package/dist/core/distribution/runtime-select.d.ts +74 -0
- package/dist/core/distribution/runtime-select.js +149 -0
- package/dist/core/execution/executor.d.ts +51 -0
- package/dist/core/execution/executor.js +106 -0
- package/dist/core/execution/grant.d.ts +42 -0
- package/dist/core/execution/grant.js +78 -0
- package/dist/core/model/entities.d.ts +872 -0
- package/dist/core/model/entities.js +285 -0
- package/dist/core/model/ids.d.ts +41 -0
- package/dist/core/model/ids.js +50 -0
- package/dist/core/model/transitions.d.ts +26 -0
- package/dist/core/model/transitions.js +124 -0
- package/dist/core/monitor/coverage.d.ts +111 -0
- package/dist/core/monitor/coverage.js +129 -0
- package/dist/core/monitor/engine.d.ts +134 -0
- package/dist/core/monitor/engine.js +574 -0
- package/dist/core/monitor/health-alerts.d.ts +35 -0
- package/dist/core/monitor/health-alerts.js +88 -0
- package/dist/core/monitor/investigation.d.ts +82 -0
- package/dist/core/monitor/investigation.js +232 -0
- package/dist/core/monitor/observation.d.ts +79 -0
- package/dist/core/monitor/observation.js +110 -0
- package/dist/core/monitor/relevance.d.ts +47 -0
- package/dist/core/monitor/relevance.js +100 -0
- package/dist/core/monitor/signals.d.ts +59 -0
- package/dist/core/monitor/signals.js +105 -0
- package/dist/core/operator/local-operator.d.ts +60 -0
- package/dist/core/operator/local-operator.js +82 -0
- package/dist/core/operator/preflight.d.ts +60 -0
- package/dist/core/operator/preflight.js +139 -0
- package/dist/core/operator/proceed.d.ts +98 -0
- package/dist/core/operator/proceed.js +167 -0
- package/dist/core/operator/progress.d.ts +124 -0
- package/dist/core/operator/progress.js +135 -0
- package/dist/core/operator/render.d.ts +34 -0
- package/dist/core/operator/render.js +170 -0
- package/dist/core/operator/runtime-binding.d.ts +61 -0
- package/dist/core/operator/runtime-binding.js +22 -0
- package/dist/core/policy/ownership.d.ts +44 -0
- package/dist/core/policy/ownership.js +46 -0
- package/dist/core/policy/policy.d.ts +99 -0
- package/dist/core/policy/policy.js +147 -0
- package/dist/core/policy/remote-freeze.d.ts +101 -0
- package/dist/core/policy/remote-freeze.js +151 -0
- package/dist/core/policy/scope.d.ts +23 -0
- package/dist/core/policy/scope.js +93 -0
- package/dist/core/presentation/digest.d.ts +98 -0
- package/dist/core/presentation/digest.js +160 -0
- package/dist/core/resolver/load.d.ts +121 -0
- package/dist/core/resolver/load.js +246 -0
- package/dist/core/resolver/render.d.ts +4 -0
- package/dist/core/resolver/render.js +62 -0
- package/dist/core/resolver/resolve.d.ts +42 -0
- package/dist/core/resolver/resolve.js +51 -0
- package/dist/core/resolver/version.d.ts +22 -0
- package/dist/core/resolver/version.js +49 -0
- package/dist/core/runtime/audit.d.ts +374 -0
- package/dist/core/runtime/audit.js +454 -0
- package/dist/core/runtime/claims.d.ts +115 -0
- package/dist/core/runtime/claims.js +153 -0
- package/dist/core/runtime/closure.d.ts +73 -0
- package/dist/core/runtime/closure.js +162 -0
- package/dist/core/runtime/controller.d.ts +40 -0
- package/dist/core/runtime/controller.js +121 -0
- package/dist/core/runtime/escalation.d.ts +188 -0
- package/dist/core/runtime/escalation.js +322 -0
- package/dist/core/runtime/execution-state.d.ts +43 -0
- package/dist/core/runtime/execution-state.js +81 -0
- package/dist/core/runtime/front.d.ts +95 -0
- package/dist/core/runtime/front.js +144 -0
- package/dist/core/runtime/orchestrator.d.ts +54 -0
- package/dist/core/runtime/orchestrator.js +98 -0
- package/dist/core/runtime/query.d.ts +184 -0
- package/dist/core/runtime/query.js +213 -0
- package/dist/core/runtime/report.d.ts +33 -0
- package/dist/core/runtime/report.js +108 -0
- package/dist/core/runtime/session.d.ts +156 -0
- package/dist/core/runtime/session.js +281 -0
- package/dist/core/runtime/store-ops.d.ts +22 -0
- package/dist/core/runtime/store-ops.js +26 -0
- package/dist/core/view/build-view.d.ts +36 -0
- package/dist/core/view/build-view.js +131 -0
- package/dist/core/view/decision-view.d.ts +564 -0
- package/dist/core/view/decision-view.js +103 -0
- package/dist/core/workspace/identity.d.ts +82 -0
- package/dist/core/workspace/identity.js +133 -0
- package/dist/core/workspace/index-store.d.ts +140 -0
- package/dist/core/workspace/index-store.js +125 -0
- package/dist/core/workspace/migrate.d.ts +73 -0
- package/dist/core/workspace/migrate.js +123 -0
- package/dist/core/workspace/resolve.d.ts +34 -0
- package/dist/core/workspace/resolve.js +89 -0
- package/dist/ports/adapter.d.ts +35 -0
- package/dist/ports/adapter.js +12 -0
- package/dist/ports/approval.d.ts +98 -0
- package/dist/ports/approval.js +7 -0
- package/dist/ports/change-context.d.ts +22 -0
- package/dist/ports/change-context.js +8 -0
- package/dist/ports/event-source.d.ts +42 -0
- package/dist/ports/event-source.js +6 -0
- package/dist/ports/inventory.d.ts +53 -0
- package/dist/ports/inventory.js +12 -0
- package/dist/ports/presentation.d.ts +62 -0
- package/dist/ports/presentation.js +13 -0
- package/dist/ports/renderer.d.ts +23 -0
- package/dist/ports/renderer.js +7 -0
- package/dist/ports/resource-context.d.ts +51 -0
- package/dist/ports/resource-context.js +9 -0
- package/dist/ports/scm.d.ts +40 -0
- package/dist/ports/scm.js +6 -0
- package/dist/ports/state-store.d.ts +97 -0
- package/dist/ports/state-store.js +12 -0
- package/dist/presets/balanced.json +8 -0
- package/dist/presets/conservative.json +9 -0
- package/dist/presets/lightweight.json +8 -0
- package/dist/profiles/example-team/profile.json +101 -0
- package/dist/profiles/pilot-local/profile.json +56 -0
- package/dist/schemas/profile.d.ts +631 -0
- package/dist/schemas/profile.js +287 -0
- package/package.json +55 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Relevance Evaluator — "나를 불렀는가"와 "실제로 내 일인가"를 따로 판정한다 (C-07 §2·§3).
|
|
2
|
+
//
|
|
3
|
+
// 신호(Signal)는 사건에 대한 관찰이고, 관련성(Relevance)은 그 사건과 지금 나·이 프로젝트의
|
|
4
|
+
// 관계다. 둘을 한 층에 두면 태깅 난사와 태깅 누락을 동시에 틀린다:
|
|
5
|
+
//
|
|
6
|
+
// 나를 불렀다 → 내 일이다 ❌ 무관한 곳에 부른 것까지 판단 대기로 올라온다
|
|
7
|
+
// 안 불렀다 → 내 일이 아니다 ❌ 지정이 빠진 실제 내 일을 놓친다
|
|
8
|
+
//
|
|
9
|
+
// 그래서 두 축을 따로 세우고, 낮은 쪽은 버리지 않고 Shadow로 내린다.
|
|
10
|
+
//
|
|
11
|
+
// 판정 근거는 **구조적 사실을 먼저** 본다. 의미 판단(제목이 내 영역 같다)은 보조다 —
|
|
12
|
+
// 그것 하나로 올리기 시작하면 왜 올라왔는지 아무도 설명할 수 없게 된다.
|
|
13
|
+
import { isWithinScopes } from "../policy/scope.js";
|
|
14
|
+
/** 나를 직접 겨눈 신호. 이것들이 explicit targeting의 전부다. */
|
|
15
|
+
const EXPLICIT = new Set([
|
|
16
|
+
'assigned_to_me',
|
|
17
|
+
'mentioned_me',
|
|
18
|
+
'direct_reply',
|
|
19
|
+
'review_requested',
|
|
20
|
+
]);
|
|
21
|
+
/**
|
|
22
|
+
* 두 축을 판정한다.
|
|
23
|
+
*
|
|
24
|
+
* **숫자를 내지 않는다** (C-07 §3.3). 0.82 같은 값은 사람이 검증할 수 없고, 틀렸을 때 어디가
|
|
25
|
+
* 틀렸는지도 말해주지 않는다. 대신 근거 문장을 남겨 사람이 뒤집을 수 있게 한다.
|
|
26
|
+
*/
|
|
27
|
+
export function evaluateRelevance(signals, context = {}) {
|
|
28
|
+
const evidence = [];
|
|
29
|
+
const explicitSignals = signals.filter((s) => EXPLICIT.has(s));
|
|
30
|
+
const explicit = explicitSignals.length > 0 ? 'HIGH' : 'LOW';
|
|
31
|
+
evidence.push(explicit === 'HIGH'
|
|
32
|
+
? { kind: 'targeting', detail: `나를 지목한 신호: ${explicitSignals.join(', ')}`, supports: true }
|
|
33
|
+
: { kind: 'targeting', detail: '나를 지목한 신호가 없다', supports: false });
|
|
34
|
+
// ① Ownership — 바뀐 경로가 내 영역인가. changed×ownership은 신호가 아니라 근거다 (C-07 §2.4).
|
|
35
|
+
const myPaths = ownedPaths(context.ownership, context.myRoles);
|
|
36
|
+
if (context.changedPaths && myPaths.length > 0) {
|
|
37
|
+
const hits = context.changedPaths.filter((path) => isWithinScopes(path, myPaths));
|
|
38
|
+
evidence.push(hits.length > 0
|
|
39
|
+
? { kind: 'ownership', detail: `내 영역 변경: ${hits.slice(0, 3).join(', ')}`, supports: true }
|
|
40
|
+
: { kind: 'ownership', detail: '바뀐 경로가 전부 내 영역 밖이다', supports: false });
|
|
41
|
+
}
|
|
42
|
+
// ② Work — 지금 돌고 있는 세션의 쓰기 범위를 건드리는가.
|
|
43
|
+
if (context.changedPaths && context.activeBoundaries?.length) {
|
|
44
|
+
for (const active of context.activeBoundaries) {
|
|
45
|
+
const hits = context.changedPaths.filter((path) => isWithinScopes(path, active.paths));
|
|
46
|
+
if (hits.length > 0) {
|
|
47
|
+
evidence.push({ kind: 'work', detail: `${active.sessionId} 와 같은 경로를 건드린다`, supports: true });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// ③ Contract — 정본 영역을 건드리는가. 신호(open_change_touches_active_canonical)와 같은
|
|
52
|
+
// 대조를 쓰지만 여기서는 관련성 근거로 다시 든다 — 신호는 "무슨 일이 있었나"이고
|
|
53
|
+
// 근거는 "그래서 내 일인가"다.
|
|
54
|
+
if (context.changedPaths && context.canonicalPaths?.length) {
|
|
55
|
+
const hits = context.changedPaths.filter((path) => isWithinScopes(path, context.canonicalPaths));
|
|
56
|
+
if (hits.length > 0) {
|
|
57
|
+
evidence.push({ kind: 'contract', detail: `정본 영역 변경: ${hits.slice(0, 3).join(', ')}`, supports: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (signals.includes('active_canonical_changed')) {
|
|
61
|
+
evidence.push({ kind: 'contract', detail: '정본 baseline이 실제로 움직였다', supports: true });
|
|
62
|
+
}
|
|
63
|
+
// ④ Participation — 내가 이미 들어가 있던 자리인가.
|
|
64
|
+
if (context.participated) {
|
|
65
|
+
evidence.push({ kind: 'participation', detail: '전에 이 스레드에 참여했다', supports: true });
|
|
66
|
+
}
|
|
67
|
+
else if (signals.includes('my_pr_reviewed')) {
|
|
68
|
+
evidence.push({ kind: 'participation', detail: '내가 만든 변경에 대한 반응이다', supports: true });
|
|
69
|
+
}
|
|
70
|
+
// ⑤ Semantic — 보조. 이것 하나로는 올리지 않는다.
|
|
71
|
+
if (context.semanticHint) {
|
|
72
|
+
evidence.push({ kind: 'semantic', detail: context.semanticHint, supports: true });
|
|
73
|
+
}
|
|
74
|
+
const structural = evidence.filter((e) => e.supports && e.kind !== 'targeting' && e.kind !== 'semantic');
|
|
75
|
+
const actual = structural.length > 0 ? 'HIGH' : 'LOW';
|
|
76
|
+
return {
|
|
77
|
+
explicit,
|
|
78
|
+
actual,
|
|
79
|
+
evidence,
|
|
80
|
+
// 실제로 관련되면 올린다. 관련 근거가 없으면 나를 불렀더라도 Shadow로 내린다 —
|
|
81
|
+
// 다만 버리지 않는다 (C-07 §5).
|
|
82
|
+
disposition: actual === 'HIGH' ? 'INBOX' : 'SHADOW',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** 이 사람이 맡은 역할들의 쓰기 영역. 역할 선언이 없으면 빈 목록이다 — 추론하지 않는다. */
|
|
86
|
+
function ownedPaths(map, roles) {
|
|
87
|
+
if (!map || !roles?.length)
|
|
88
|
+
return [];
|
|
89
|
+
return roles.flatMap((role) => map[role]?.paths ?? []);
|
|
90
|
+
}
|
|
91
|
+
/** 사람이 읽는 근거 블록. 근거 없이 결론만 보여주지 않는다. */
|
|
92
|
+
export function renderRelevance(relevance) {
|
|
93
|
+
const lines = [`Relevance: ${relevance.actual} (지목 ${relevance.explicit})`];
|
|
94
|
+
for (const item of relevance.evidence)
|
|
95
|
+
lines.push(` ${item.supports ? '+' : '-'} ${item.detail}`);
|
|
96
|
+
if (relevance.disposition === 'SHADOW') {
|
|
97
|
+
lines.push(' → Shadow Watch (숨김. 변화가 생기면 다시 본다)');
|
|
98
|
+
}
|
|
99
|
+
return lines;
|
|
100
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { EventType, Priority } from '../model/entities.ts';
|
|
2
|
+
import type { RawEvent } from '../../ports/event-source.ts';
|
|
3
|
+
export declare const GENERIC_SIGNALS: readonly ["assigned_to_me", "mentioned_me", "direct_reply", "review_requested", "my_pr_reviewed", "participated_thread_changed", "active_canonical_changed", "open_change_touches_active_canonical", "priority_labels", "project_specific_signal"];
|
|
4
|
+
export type GenericSignal = (typeof GENERIC_SIGNALS)[number];
|
|
5
|
+
/**
|
|
6
|
+
* 신호를 판정하는 데 필요한 프로젝트별 값. 선언형이어야 Profile(B-10)이 그대로 채울 수 있다 —
|
|
7
|
+
* 여기에 함수를 받기 시작하면 설정 파일로는 표현할 수 없는 물건이 된다.
|
|
8
|
+
*/
|
|
9
|
+
export type MonitorConfig = {
|
|
10
|
+
/** "나"로 인정할 계정들. 개인 값이므로 User Override 몫이다 (OM §4.4). */
|
|
11
|
+
identities?: readonly string[];
|
|
12
|
+
/**
|
|
13
|
+
* provider가 주는 사유 → 신호. Core에 기본값을 두지 않는다 — 어느 provider의 `mention`이든
|
|
14
|
+
* 다른 곳의 무엇이든 그건 provider의 어휘이고, 여기 박아 두면 Core가 특정 provider를
|
|
15
|
+
* 아는 물건이 된다. 매핑은 Adapter가 내놓고 Profile이 고른다.
|
|
16
|
+
*/
|
|
17
|
+
reasonSignals?: Readonly<Record<string, GenericSignal>>;
|
|
18
|
+
/** 라벨 → 우선순위. */
|
|
19
|
+
priorityLabels?: Readonly<Record<string, Priority>>;
|
|
20
|
+
/** 붙어 있으면 한 단계 올린다. */
|
|
21
|
+
escalationLabels?: readonly string[];
|
|
22
|
+
/** 신호별 기본 우선순위. */
|
|
23
|
+
signalPriority?: Partial<Record<GenericSignal, Priority>>;
|
|
24
|
+
/** 이 신호 중 하나라도 있으면 사람의 판단 대기함에 올린다. 비우면 신호가 있는 것 전부. */
|
|
25
|
+
inboxSignals?: readonly GenericSignal[];
|
|
26
|
+
};
|
|
27
|
+
export type Classification = {
|
|
28
|
+
signals: GenericSignal[];
|
|
29
|
+
type: EventType;
|
|
30
|
+
priority: Priority;
|
|
31
|
+
inboxCandidate: boolean;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* 신호를 세우는 데 필요한 **관찰된 사실**. Config와 나누는 이유는 성격이 다르기 때문이다 —
|
|
35
|
+
* Config는 프로젝트가 미리 적어 두는 선언이고, 이쪽은 이번 사건을 보고 알게 된 것이다.
|
|
36
|
+
* 여기에 함수를 받지 않는 것도 같은 이유다 (설정 파일로 표현할 수 없는 물건이 되지 않게).
|
|
37
|
+
*
|
|
38
|
+
* 전부 optional이다 — 모르면 그 신호를 세우지 않는다. 모르는 것을 false로 적으면
|
|
39
|
+
* "안 건드렸다"는 틀린 판정이 된다.
|
|
40
|
+
*/
|
|
41
|
+
export type SignalContext = {
|
|
42
|
+
/**
|
|
43
|
+
* 나에게 배정됐는가. 목록 조회로 알게 되는 사실이라 알림 사유와 별개다 —
|
|
44
|
+
* 지정이 알림으로 오지 않는 경우가 정확히 회수 경로가 필요한 이유다.
|
|
45
|
+
*/
|
|
46
|
+
assignedToMe?: boolean;
|
|
47
|
+
/** 내 글에 달린 응답인가. */
|
|
48
|
+
replyToMe?: boolean;
|
|
49
|
+
/** 정본 baseline이 실제로 움직였는가 — actual drift. */
|
|
50
|
+
canonicalChanged?: boolean;
|
|
51
|
+
/** 이번 변경이 건드린 실제 경로. */
|
|
52
|
+
changedPaths?: readonly string[];
|
|
53
|
+
/** 정본이 사는 경로. 위와 겹치면 potential drift다. */
|
|
54
|
+
canonicalPaths?: readonly string[];
|
|
55
|
+
};
|
|
56
|
+
/** 이 이벤트에 어떤 신호가 걸리는가. 순서는 판정에 영향이 없다. */
|
|
57
|
+
export declare function detectSignals(event: RawEvent, config: MonitorConfig, context?: SignalContext): GenericSignal[];
|
|
58
|
+
/** 신호를 유형·우선순위·수신함 여부로 옮긴다. Phase A의 마지막 단계다. */
|
|
59
|
+
export declare function classify(event: RawEvent, config: MonitorConfig, context?: SignalContext): Classification;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Generic Signal — Core가 아는 신호의 전부 (OM §10.6).
|
|
2
|
+
//
|
|
3
|
+
// 여기에 계정 이름도, 라벨 이름도, 저장소 이름도 없다. "나에게 배정됐다"는 개념은 알지만
|
|
4
|
+
// 누가 나인지는 모르고, "우선순위 라벨"이라는 개념은 알지만 어떤 이름의 라벨인지는 모른다.
|
|
5
|
+
// 실제 값은 Profile과 User Override가 채운다 — 그래야 같은 Core가 다른 프로젝트에 붙는다.
|
|
6
|
+
import { isWithinScopes } from "../policy/scope.js";
|
|
7
|
+
export const GENERIC_SIGNALS = [
|
|
8
|
+
'assigned_to_me',
|
|
9
|
+
'mentioned_me',
|
|
10
|
+
'direct_reply',
|
|
11
|
+
'review_requested',
|
|
12
|
+
'my_pr_reviewed',
|
|
13
|
+
'participated_thread_changed',
|
|
14
|
+
'active_canonical_changed',
|
|
15
|
+
'open_change_touches_active_canonical',
|
|
16
|
+
'priority_labels',
|
|
17
|
+
'project_specific_signal',
|
|
18
|
+
];
|
|
19
|
+
const DEFAULT_SIGNAL_PRIORITY = {
|
|
20
|
+
assigned_to_me: 'P0',
|
|
21
|
+
mentioned_me: 'P0',
|
|
22
|
+
direct_reply: 'P0',
|
|
23
|
+
review_requested: 'P1',
|
|
24
|
+
my_pr_reviewed: 'P1',
|
|
25
|
+
participated_thread_changed: 'P1',
|
|
26
|
+
active_canonical_changed: 'P1',
|
|
27
|
+
open_change_touches_active_canonical: 'P2',
|
|
28
|
+
priority_labels: 'P1',
|
|
29
|
+
project_specific_signal: 'P2',
|
|
30
|
+
};
|
|
31
|
+
/** 답을 해야 하는 신호. 나머지는 알아두면 되는 것이다 (OM §10.3). */
|
|
32
|
+
const ACTIONABLE = new Set(['mentioned_me', 'direct_reply', 'review_requested']);
|
|
33
|
+
const WORK = new Set(['assigned_to_me']);
|
|
34
|
+
const RANK = ['P0', 'P1', 'P2'];
|
|
35
|
+
const higher = (a, b) => (RANK.indexOf(a) <= RANK.indexOf(b) ? a : b);
|
|
36
|
+
const raise = (p) => RANK[Math.max(0, RANK.indexOf(p) - 1)];
|
|
37
|
+
function mentionsMe(text, identities) {
|
|
38
|
+
if (!text)
|
|
39
|
+
return false;
|
|
40
|
+
return identities.some((id) => text.includes(`@${id.replace(/^@/, '')}`));
|
|
41
|
+
}
|
|
42
|
+
/** 이 이벤트에 어떤 신호가 걸리는가. 순서는 판정에 영향이 없다. */
|
|
43
|
+
export function detectSignals(event, config, context = {}) {
|
|
44
|
+
const identities = config.identities ?? [];
|
|
45
|
+
const raw = (event.raw ?? {});
|
|
46
|
+
const signals = new Set();
|
|
47
|
+
if (raw.reason) {
|
|
48
|
+
const mapped = config.reasonSignals?.[raw.reason];
|
|
49
|
+
if (mapped)
|
|
50
|
+
signals.add(mapped);
|
|
51
|
+
}
|
|
52
|
+
// 본문에 내 이름이 불렸다. 알림이 오지 않는 경로(참여하지 않은 스레드)에서도 잡힌다.
|
|
53
|
+
if (mentionsMe(raw.body, identities))
|
|
54
|
+
signals.add('mentioned_me');
|
|
55
|
+
const labels = event.hints?.labels ?? [];
|
|
56
|
+
if (labels.some((label) => config.priorityLabels?.[label] !== undefined))
|
|
57
|
+
signals.add('priority_labels');
|
|
58
|
+
if (context.assignedToMe)
|
|
59
|
+
signals.add('assigned_to_me');
|
|
60
|
+
// 내 글에 달린 응답. provider의 알림 사유로는 mention과 구분되지 않는 경우가 많아
|
|
61
|
+
// 관찰된 사실로 받는다.
|
|
62
|
+
if (context.replyToMe)
|
|
63
|
+
signals.add('direct_reply');
|
|
64
|
+
// 정본이 **이미** 움직인 것과, 열린 변경이 정본 영역을 **건드리는** 것은 다른 사건이다
|
|
65
|
+
// (C-07 §2.4). 둘을 합치면 "바뀌었다"와 "바뀔 수 있다"를 구분할 수 없게 된다.
|
|
66
|
+
if (context.canonicalChanged)
|
|
67
|
+
signals.add('active_canonical_changed');
|
|
68
|
+
if (context.changedPaths?.length &&
|
|
69
|
+
context.canonicalPaths?.length &&
|
|
70
|
+
context.changedPaths.some((path) => isWithinScopes(path, context.canonicalPaths))) {
|
|
71
|
+
signals.add('open_change_touches_active_canonical');
|
|
72
|
+
}
|
|
73
|
+
// 내가 쓴 글은 나에게 알릴 일이 아니다 — 다른 신호가 없다면 조용히 넘긴다.
|
|
74
|
+
const authors = event.hints?.actors ?? [];
|
|
75
|
+
const mine = authors.length > 0 && authors.every((a) => identities.includes(a));
|
|
76
|
+
if (mine && signals.size === 0)
|
|
77
|
+
return [];
|
|
78
|
+
return [...signals];
|
|
79
|
+
}
|
|
80
|
+
/** 신호를 유형·우선순위·수신함 여부로 옮긴다. Phase A의 마지막 단계다. */
|
|
81
|
+
export function classify(event, config, context = {}) {
|
|
82
|
+
const signals = detectSignals(event, config, context);
|
|
83
|
+
const type = signals.some((s) => WORK.has(s))
|
|
84
|
+
? 'work'
|
|
85
|
+
: signals.some((s) => ACTIONABLE.has(s))
|
|
86
|
+
? 'actionable'
|
|
87
|
+
: 'informational';
|
|
88
|
+
let priority = 'P2';
|
|
89
|
+
for (const signal of signals) {
|
|
90
|
+
priority = higher(priority, config.signalPriority?.[signal] ?? DEFAULT_SIGNAL_PRIORITY[signal]);
|
|
91
|
+
}
|
|
92
|
+
for (const label of event.hints?.labels ?? []) {
|
|
93
|
+
const mapped = config.priorityLabels?.[label];
|
|
94
|
+
if (mapped)
|
|
95
|
+
priority = higher(priority, mapped);
|
|
96
|
+
}
|
|
97
|
+
if ((event.hints?.labels ?? []).some((label) => (config.escalationLabels ?? []).includes(label))) {
|
|
98
|
+
priority = raise(priority);
|
|
99
|
+
}
|
|
100
|
+
// provider가 힌트를 줬다면 참고는 하되, 올리는 쪽으로만 쓴다
|
|
101
|
+
if (event.hints?.priority)
|
|
102
|
+
priority = higher(priority, event.hints.priority);
|
|
103
|
+
const inboxCandidate = signals.length > 0 && (config.inboxSignals === undefined || signals.some((s) => config.inboxSignals.includes(s)));
|
|
104
|
+
return { signals, type, priority, inboxCandidate };
|
|
105
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ScmPort } from '../../ports/scm.ts';
|
|
2
|
+
import type { HistoryEntry, StateStore } from '../../ports/state-store.ts';
|
|
3
|
+
import type { DecisionSummary, DecisionView } from '../view/decision-view.ts';
|
|
4
|
+
export type ListOptions = {
|
|
5
|
+
/** true면 처분된 것까지 전부 (기본은 판단 대기만). */
|
|
6
|
+
all?: boolean;
|
|
7
|
+
priority?: 'P0' | 'P1' | 'P2';
|
|
8
|
+
limit?: number;
|
|
9
|
+
};
|
|
10
|
+
export type GetOutcome = {
|
|
11
|
+
ok: true;
|
|
12
|
+
view: DecisionView;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
reason: 'NOT_FOUND';
|
|
16
|
+
requestId: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* 참조가 하나로 좁혀지지 않을 때의 결과. Agent가 임의로 하나를 고르지 않도록
|
|
20
|
+
* 후보를 그대로 돌려준다 (C-01 §11).
|
|
21
|
+
*/
|
|
22
|
+
export type ResolveOutcome = {
|
|
23
|
+
kind: 'resolved';
|
|
24
|
+
view: DecisionView;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'ambiguous';
|
|
27
|
+
candidates: DecisionSummary[];
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'none';
|
|
30
|
+
};
|
|
31
|
+
export type OperatorDeps = {
|
|
32
|
+
store: StateStore;
|
|
33
|
+
/** 없어도 동작한다 — 정본·스레드 확인만 건너뛴다. */
|
|
34
|
+
scm?: ScmPort;
|
|
35
|
+
/** 테스트에서 시각을 고정하기 위한 주입점. */
|
|
36
|
+
now?: () => string;
|
|
37
|
+
};
|
|
38
|
+
export declare class LocalOperator {
|
|
39
|
+
#private;
|
|
40
|
+
constructor(deps: OperatorDeps);
|
|
41
|
+
/** 최근 감지 순. 목록에도 freshness를 붙여 이미 결정된 것을 눈에 띄게 한다. */
|
|
42
|
+
list(options?: ListOptions): Promise<DecisionSummary[]>;
|
|
43
|
+
/** 하나를 지목해 전체 보고서를 만든다 — stored와 current를 함께 담는다. */
|
|
44
|
+
get(requestId: string): Promise<GetOutcome>;
|
|
45
|
+
/**
|
|
46
|
+
* "방금 온 알림 보여줘" — request_id 없이 최근 것을 찾는다.
|
|
47
|
+
* 후보가 여럿이면 하나를 고르지 않고 그대로 돌려준다. 잘못 고른 요청을 승인 화면까지
|
|
48
|
+
* 끌고 가는 것보다 한 번 더 묻는 편이 싸다.
|
|
49
|
+
*/
|
|
50
|
+
resolveLatest(options?: ListOptions): Promise<ResolveOutcome>;
|
|
51
|
+
/**
|
|
52
|
+
* 이 요청이 지금 상태가 된 경위 (C-05 §3.2 trace).
|
|
53
|
+
*
|
|
54
|
+
* 새 저장 구조를 만들지 않는다 — depth는 "얼마나 읽을 것인가"의 계약이지 새 데이터가
|
|
55
|
+
* 아니고, 감지와 최종 처분의 이력은 이미 History Log에 있다 (OM §11.2).
|
|
56
|
+
*
|
|
57
|
+
* 여기도 읽기뿐이다. 경위를 아무리 깊게 읽어도 결정 제출 경로는 열리지 않는다 (C-01 §5).
|
|
58
|
+
*/
|
|
59
|
+
trace(requestId: string, limit?: number): Promise<HistoryEntry[]>;
|
|
60
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Local Operator Interface — 로컬 작업환경이 ASC 요청을 조회하는 표면 (C-01 §4).
|
|
2
|
+
//
|
|
3
|
+
// Core는 어떤 도구가 이걸 부르는지 모른다. CLI든 MCP든 IDE 확장이든 전부 소비자일 뿐이고,
|
|
4
|
+
// 제품 이름이 이 파일에 들어오는 순간 계약이 깨진다.
|
|
5
|
+
//
|
|
6
|
+
// 여기 있는 것은 읽기뿐이다. 결정 제출은 사람의 명시적 의사표현을 받은 뒤에만 일어나므로
|
|
7
|
+
// 별도 경로(B-06)로 나간다 — Agent가 조회하다가 이어서 승인해버릴 수 있는 문을 만들지 않는다 (C-01 §5).
|
|
8
|
+
import { assembleView, assess, buildOverlay, summarize } from "../view/build-view.js";
|
|
9
|
+
/** 아직 사람의 판단을 기다리는 상태. 목록의 기본 필터다. */
|
|
10
|
+
const PENDING = new Set(['AWAITING_APPROVAL', 'APPROVED']);
|
|
11
|
+
export class LocalOperator {
|
|
12
|
+
#store;
|
|
13
|
+
#scm;
|
|
14
|
+
#now;
|
|
15
|
+
constructor(deps) {
|
|
16
|
+
this.#store = deps.store;
|
|
17
|
+
this.#scm = deps.scm;
|
|
18
|
+
this.#now = deps.now ?? (() => new Date().toISOString());
|
|
19
|
+
}
|
|
20
|
+
/** 최근 감지 순. 목록에도 freshness를 붙여 이미 결정된 것을 눈에 띄게 한다. */
|
|
21
|
+
async list(options = {}) {
|
|
22
|
+
const requests = await this.#store.list('request');
|
|
23
|
+
const filtered = requests
|
|
24
|
+
.filter((r) => options.all || PENDING.has(r.status))
|
|
25
|
+
.filter((r) => !options.priority || r.priority === options.priority)
|
|
26
|
+
.sort((a, b) => b.detectedAt.localeCompare(a.detectedAt))
|
|
27
|
+
.slice(0, options.limit ?? requests.length);
|
|
28
|
+
const summaries = [];
|
|
29
|
+
for (const request of filtered)
|
|
30
|
+
summaries.push(summarize(request, await this.#freshness(request)));
|
|
31
|
+
return summaries;
|
|
32
|
+
}
|
|
33
|
+
/** 하나를 지목해 전체 보고서를 만든다 — stored와 current를 함께 담는다. */
|
|
34
|
+
async get(requestId) {
|
|
35
|
+
const request = await this.#store.get('request', requestId);
|
|
36
|
+
if (!request)
|
|
37
|
+
return { ok: false, reason: 'NOT_FOUND', requestId };
|
|
38
|
+
return { ok: true, view: await this.#view(request) };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* "방금 온 알림 보여줘" — request_id 없이 최근 것을 찾는다.
|
|
42
|
+
* 후보가 여럿이면 하나를 고르지 않고 그대로 돌려준다. 잘못 고른 요청을 승인 화면까지
|
|
43
|
+
* 끌고 가는 것보다 한 번 더 묻는 편이 싸다.
|
|
44
|
+
*/
|
|
45
|
+
async resolveLatest(options = {}) {
|
|
46
|
+
const candidates = await this.list({ ...options, limit: undefined });
|
|
47
|
+
if (candidates.length === 0)
|
|
48
|
+
return { kind: 'none' };
|
|
49
|
+
if (candidates.length > 1)
|
|
50
|
+
return { kind: 'ambiguous', candidates };
|
|
51
|
+
const outcome = await this.get(candidates[0].requestId);
|
|
52
|
+
return outcome.ok ? { kind: 'resolved', view: outcome.view } : { kind: 'none' };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 이 요청이 지금 상태가 된 경위 (C-05 §3.2 trace).
|
|
56
|
+
*
|
|
57
|
+
* 새 저장 구조를 만들지 않는다 — depth는 "얼마나 읽을 것인가"의 계약이지 새 데이터가
|
|
58
|
+
* 아니고, 감지와 최종 처분의 이력은 이미 History Log에 있다 (OM §11.2).
|
|
59
|
+
*
|
|
60
|
+
* 여기도 읽기뿐이다. 경위를 아무리 깊게 읽어도 결정 제출 경로는 열리지 않는다 (C-01 §5).
|
|
61
|
+
*/
|
|
62
|
+
async trace(requestId, limit) {
|
|
63
|
+
const entries = await this.#store.readHistory(limit);
|
|
64
|
+
return entries.filter((entry) => entry.ref === requestId);
|
|
65
|
+
}
|
|
66
|
+
async #view(request) {
|
|
67
|
+
const overlay = await buildOverlay(request, {
|
|
68
|
+
control: await this.#store.getControlState(),
|
|
69
|
+
observedAt: this.#now(),
|
|
70
|
+
...(this.#scm ? { scm: this.#scm } : {}),
|
|
71
|
+
});
|
|
72
|
+
return assembleView(request, await assess(request, overlay, this.#scm), overlay);
|
|
73
|
+
}
|
|
74
|
+
async #freshness(request) {
|
|
75
|
+
const overlay = await buildOverlay(request, {
|
|
76
|
+
control: await this.#store.getControlState(),
|
|
77
|
+
observedAt: this.#now(),
|
|
78
|
+
...(this.#scm ? { scm: this.#scm } : {}),
|
|
79
|
+
});
|
|
80
|
+
return (await assess(request, overlay, this.#scm)).freshness;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type AuthorityLookup, type OwnershipMap } from '../policy/ownership.ts';
|
|
2
|
+
import type { ResolvedPolicy, RoleName } from '../policy/policy.ts';
|
|
3
|
+
/** 무엇과 대조하는가. role은 최대 범위, session은 실제 계약 — 후자가 더 좁을 수 있다. */
|
|
4
|
+
export type PreflightTarget = {
|
|
5
|
+
kind: 'role';
|
|
6
|
+
role: RoleName;
|
|
7
|
+
maxScope: readonly string[] | undefined;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'session';
|
|
10
|
+
sessionId: string;
|
|
11
|
+
role?: RoleName;
|
|
12
|
+
writeBoundary: readonly string[];
|
|
13
|
+
/** 책임 축 (C-04 §2). 선언하지 않은 세션에서는 이 축을 판정하지 않는다. */
|
|
14
|
+
owner?: string;
|
|
15
|
+
decisionDomains?: readonly string[];
|
|
16
|
+
decisionAuthority?: Readonly<Record<string, string>>;
|
|
17
|
+
};
|
|
18
|
+
export type PathVerdict = {
|
|
19
|
+
path: string;
|
|
20
|
+
verdict: 'OK';
|
|
21
|
+
}
|
|
22
|
+
/** 문법 밖 경로. 통과시키지 않는다 — 판정할 수 없는 것은 통과가 아니다. */
|
|
23
|
+
| {
|
|
24
|
+
path: string;
|
|
25
|
+
verdict: 'INVALID_SCOPE';
|
|
26
|
+
} | {
|
|
27
|
+
path: string;
|
|
28
|
+
verdict: 'BOUNDARY_MISMATCH';
|
|
29
|
+
}
|
|
30
|
+
/** 쓰기 범위 안이지만 owner의 영역 밖이다 — 남의 파트 산출물을 만들고 있다. */
|
|
31
|
+
| {
|
|
32
|
+
path: string;
|
|
33
|
+
verdict: 'OWNERSHIP_MISMATCH';
|
|
34
|
+
};
|
|
35
|
+
export type PreflightResult = {
|
|
36
|
+
target: PreflightTarget;
|
|
37
|
+
verdicts: PathVerdict[];
|
|
38
|
+
mismatches: PathVerdict[];
|
|
39
|
+
/** 사람이 읽을 대안. 문자열뿐이다 — 실행 가능한 명령을 자동으로 태우지 않는다. */
|
|
40
|
+
suggestions: string[];
|
|
41
|
+
/**
|
|
42
|
+
* 주인이 정해지지 않은 결정 영역. 경로 축(PathVerdict)과 다른 축이라 따로 든다 —
|
|
43
|
+
* 결정은 파일에 붙지 않으므로 경로별 판정에 섞을 수 없다.
|
|
44
|
+
*/
|
|
45
|
+
authorityGaps: {
|
|
46
|
+
domain: string;
|
|
47
|
+
lookup: AuthorityLookup;
|
|
48
|
+
}[];
|
|
49
|
+
/** 판정이 성립하지 않은 이유. 있으면 결과를 "통과"로 읽으면 안 된다. */
|
|
50
|
+
undecidable?: string;
|
|
51
|
+
};
|
|
52
|
+
export type PreflightInput = {
|
|
53
|
+
paths: readonly string[];
|
|
54
|
+
target: PreflightTarget;
|
|
55
|
+
/** role 후보 제안용. 없으면 후보를 내지 않는다. */
|
|
56
|
+
policy?: ResolvedPolicy;
|
|
57
|
+
/** Profile이 선언한 책임 지도 (C-04 §6). 없으면 책임 축은 판정 불성립이다. */
|
|
58
|
+
ownership?: OwnershipMap;
|
|
59
|
+
};
|
|
60
|
+
export declare function preflight(input: PreflightInput): PreflightResult;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Boundary Preflight — 다른 role에게 일을 넘기기 전에 경로를 맞춰 본다 (B-19).
|
|
2
|
+
//
|
|
3
|
+
// 근거 사건(B-16 T015): planner가 tasks.md에 산출 경로 `specs/.../quickstart.md`를 적었는데
|
|
4
|
+
// 그 일을 할 implementer의 쓰기 범위는 `web-frontend/**`였다. 불일치는 실행 단계에서야
|
|
5
|
+
// 드러났고, 해소하는 데 세션을 하나 더 발급해야 했다. 대조 자체는 1초면 되는데 그걸
|
|
6
|
+
// 나중에 한 것이 비용이었다.
|
|
7
|
+
//
|
|
8
|
+
// 이 모듈이 하는 일과 하지 않는 일:
|
|
9
|
+
// 한다 — 경로가 대상 범위 안에 드는지 판정하고, 어긋나면 대안을 문장으로 제시
|
|
10
|
+
// 안 한다 — 범위 확장, role 변경, 세션 발급. 판정은 여기서, 결정은 Controller가.
|
|
11
|
+
//
|
|
12
|
+
// 판정 로직은 새로 만들지 않는다. scope.ts의 패턴 대 패턴 판정을 그대로 쓴다 —
|
|
13
|
+
// policy.evaluate()를 쓰지 않는 이유는 그쪽이 단수 path + 경로 대 패턴이라
|
|
14
|
+
// `specs/**` 같은 패턴 입력을 오판하기 때문이다.
|
|
15
|
+
import { lookupAuthority } from "../policy/ownership.js";
|
|
16
|
+
import { isWithinScopes, parseScope } from "../policy/scope.js";
|
|
17
|
+
export function preflight(input) {
|
|
18
|
+
const { paths, target, policy, ownership } = input;
|
|
19
|
+
const boundary = target.kind === 'role' ? target.maxScope : target.writeBoundary;
|
|
20
|
+
// 정책에 role이 선언돼 있지 않으면 최대 범위를 모른다. 모르는 것을 통과로 만들지 않는다 —
|
|
21
|
+
// Session issue의 SCOPE_ESCALATION 검사도 같은 자리에서 검사를 건너뛰므로(session.ts),
|
|
22
|
+
// preflight까지 조용히 통과시키면 두 곳 다 아무 말이 없게 된다.
|
|
23
|
+
if (boundary === undefined) {
|
|
24
|
+
return {
|
|
25
|
+
target,
|
|
26
|
+
verdicts: [],
|
|
27
|
+
mismatches: [],
|
|
28
|
+
suggestions: [],
|
|
29
|
+
authorityGaps: [],
|
|
30
|
+
undecidable: target.kind === 'role'
|
|
31
|
+
? `Profile에 '${target.role}' 역할의 쓰기 범위가 선언돼 있지 않다 — 대조할 기준이 없다`
|
|
32
|
+
: `${target.sessionId} 의 쓰기 범위를 읽지 못했다`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// owner의 영역. 선언한 세션에서만 쓴다 — role 대조에는 owner가 없다.
|
|
36
|
+
const ownerPaths = target.kind === 'session' && target.owner ? ownership?.[target.owner]?.paths : undefined;
|
|
37
|
+
const verdicts = paths.map((path) => {
|
|
38
|
+
if (parseScope(path) === null)
|
|
39
|
+
return { path, verdict: 'INVALID_SCOPE' };
|
|
40
|
+
if (!isWithinScopes(path, boundary))
|
|
41
|
+
return { path, verdict: 'BOUNDARY_MISMATCH' };
|
|
42
|
+
// 쓰기 범위는 "이 계약이 어디까지 쓸 수 있는가"이고, 여기는 "그게 누구 영역인가"다.
|
|
43
|
+
// 둘 다 통과해야 남의 파트를 대신 만들고 있지 않다고 말할 수 있다.
|
|
44
|
+
if (ownerPaths && ownerPaths.length > 0 && !isWithinScopes(path, ownerPaths)) {
|
|
45
|
+
return { path, verdict: 'OWNERSHIP_MISMATCH' };
|
|
46
|
+
}
|
|
47
|
+
return { path, verdict: 'OK' };
|
|
48
|
+
});
|
|
49
|
+
const mismatches = verdicts.filter((v) => v.verdict !== 'OK');
|
|
50
|
+
const authorityGaps = findAuthorityGaps(target, ownership);
|
|
51
|
+
const result = {
|
|
52
|
+
target,
|
|
53
|
+
verdicts,
|
|
54
|
+
mismatches,
|
|
55
|
+
authorityGaps,
|
|
56
|
+
suggestions: mismatches.length === 0 && authorityGaps.length === 0
|
|
57
|
+
? []
|
|
58
|
+
: suggest(paths, verdicts, target, boundary, policy, authorityGaps),
|
|
59
|
+
};
|
|
60
|
+
// 책임을 물었는데 지도가 없다. 답이 없는 것과 "괜찮다"는 것을 구분한다.
|
|
61
|
+
if (target.kind === 'session' && !ownership && (target.owner || target.decisionDomains?.length)) {
|
|
62
|
+
result.undecidable = 'Profile에 책임 지도(ownership)가 선언돼 있지 않다 — 책임 축을 대조할 기준이 없다';
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
/** 주인이 하나로 정해지지 않은 결정 영역만 골라낸다. 고르지는 않는다. */
|
|
67
|
+
function findAuthorityGaps(target, ownership) {
|
|
68
|
+
if (target.kind !== 'session')
|
|
69
|
+
return [];
|
|
70
|
+
const gaps = [];
|
|
71
|
+
for (const domain of target.decisionDomains ?? []) {
|
|
72
|
+
if (target.decisionAuthority?.[domain])
|
|
73
|
+
continue;
|
|
74
|
+
const lookup = lookupAuthority(ownership, domain);
|
|
75
|
+
if (lookup.kind !== 'RESOLVED')
|
|
76
|
+
gaps.push({ domain, lookup });
|
|
77
|
+
}
|
|
78
|
+
return gaps;
|
|
79
|
+
}
|
|
80
|
+
function suggest(paths, verdicts, target, boundary, policy, authorityGaps) {
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const gap of authorityGaps) {
|
|
83
|
+
out.push(gap.lookup.kind === 'AMBIGUOUS'
|
|
84
|
+
? `'${gap.domain}' 의 결정권자가 갈려 있다 (${gap.lookup.candidates.join(', ')}) — 발급 전에 하나로 정하라.`
|
|
85
|
+
: `'${gap.domain}' 의 결정권자가 선언되지 않았다 — Profile ownership 에 적거나 세션 계약에 명시하라.`);
|
|
86
|
+
}
|
|
87
|
+
if (authorityGaps.length > 0) {
|
|
88
|
+
out.push('결정권자가 없는 채로 cross-part를 시작하면 답이 Agent 사이를 돈다. 먼저 정한다.');
|
|
89
|
+
}
|
|
90
|
+
if (verdicts.some((v) => v.verdict === 'OWNERSHIP_MISMATCH') && target.kind === 'session') {
|
|
91
|
+
out.push(`쓰기 범위 안이지만 '${target.owner}' 의 영역 밖인 경로가 있다 — 남의 파트 산출물이라면 ` +
|
|
92
|
+
'그 파트가 만들고, 이 세션은 필요한 것을 질의로 받는다.');
|
|
93
|
+
}
|
|
94
|
+
if (verdicts.some((v) => v.verdict === 'INVALID_SCOPE')) {
|
|
95
|
+
out.push('문법 밖 경로를 먼저 고쳐라 — 허용 형태는 `**`, `p/**`, `p/*`, 정확한 경로다.');
|
|
96
|
+
}
|
|
97
|
+
// 쓰기 범위가 비면 경로를 옮기는 게 아니라 대상 선택이 잘못된 것이다.
|
|
98
|
+
// 다만 "이 역할은 파일을 만들지 않는다"고 단정하지 않는다 — 그건 역할의 본질이 아니라
|
|
99
|
+
// 지금 정책이 그렇게 정해 둔 상태다. Profile에 값만 있고 설명이 없어 여기서 말해 준다.
|
|
100
|
+
if (boundary.length === 0) {
|
|
101
|
+
out.push(target.kind === 'role'
|
|
102
|
+
? `'${target.role}' 에게 현재 허용된 쓰기 범위가 없다 — 지금 정책에서는 이 역할로 파일을 만들 수 없다. 산출물이 필요하면 다른 역할이 맡거나 정책을 바꿔야 한다.`
|
|
103
|
+
: `${target.sessionId} 에 현재 허용된 쓰기 범위가 없다 — 이 계약으로는 어떤 파일도 만들 수 없다.`);
|
|
104
|
+
}
|
|
105
|
+
const candidates = rolesCovering(paths, policy, target);
|
|
106
|
+
if (candidates.length > 0) {
|
|
107
|
+
out.push(`산출 경로 전체를 최대 범위 안에 두는 역할: ${candidates.join(', ')}. ` +
|
|
108
|
+
'다만 이는 **역할의 최대 범위** 기준이다 — 실제 세션의 쓰기 범위는 발급 시 더 좁게 정해질 수 있으므로, ' +
|
|
109
|
+
'발급 후 `asc preflight --session <S-ID>` 로 다시 확인하라.');
|
|
110
|
+
}
|
|
111
|
+
else if (policy) {
|
|
112
|
+
out.push('이 산출 경로들을 한 역할이 통째로 맡을 수 없다 — 경로별로 세션을 나누거나, ' +
|
|
113
|
+
'산출 경로를 수행 역할의 범위 안으로 옮겨라.');
|
|
114
|
+
}
|
|
115
|
+
out.push('세션을 나눈다 — 범위가 다른 산출물은 각자의 계약으로 발급한다.');
|
|
116
|
+
out.push('산출 경로를 옮긴다 — 수행 역할의 범위 안에 두면 세션을 늘리지 않아도 된다.');
|
|
117
|
+
out.push('쓰기 범위를 넓혀 해소하지 않는다. 권한 확대는 Controller의 명시적 결정이다.');
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 경로 **전부**를 최대 범위에 담는 역할만 후보다. 일부만 담는 역할을 권하면
|
|
122
|
+
* 그 역할로 바꿔도 다시 갈라져 같은 사건이 반복된다.
|
|
123
|
+
*/
|
|
124
|
+
function rolesCovering(paths, policy, target) {
|
|
125
|
+
if (!policy)
|
|
126
|
+
return [];
|
|
127
|
+
const valid = paths.filter((p) => parseScope(p) !== null);
|
|
128
|
+
if (valid.length === 0)
|
|
129
|
+
return [];
|
|
130
|
+
return Object.entries(policy.roleScopes)
|
|
131
|
+
.filter(([role, scope]) => {
|
|
132
|
+
if (target.kind === 'role' && role === target.role)
|
|
133
|
+
return false; // 지금 어긋난 그 역할
|
|
134
|
+
if (!scope || scope.length === 0)
|
|
135
|
+
return false;
|
|
136
|
+
return valid.every((p) => isWithinScopes(p, scope));
|
|
137
|
+
})
|
|
138
|
+
.map(([role]) => role);
|
|
139
|
+
}
|