@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,281 @@
|
|
|
1
|
+
// Session lifecycle 오케스트레이션 — 발급부터 인수인계까지.
|
|
2
|
+
// Logical Session은 여러 Physical Run에 걸친다 (OM §6.2): 한 Run이 Checkpoint를 남기고
|
|
3
|
+
// 멈추면 다른 Run이 같은 계약을 이어받는다. 그 승계가 가능한지가 이 파일의 존재 이유다.
|
|
4
|
+
//
|
|
5
|
+
// Attach Pilot(B-11)에서 세션 기능을 처음 만들지 않도록 여기서 미리 검증한다.
|
|
6
|
+
import { Session } from "../model/entities.js";
|
|
7
|
+
import { transitionSession } from "../model/transitions.js";
|
|
8
|
+
import { lookupAuthority } from "../policy/ownership.js";
|
|
9
|
+
import { isWithinScopes, parseScope } from "../policy/scope.js";
|
|
10
|
+
import { applyTransition } from "./store-ops.js";
|
|
11
|
+
/** `unknown`은 값이 아니라 "못 읽었다"는 표시다 (ports/scm.ts). */
|
|
12
|
+
const UNREADABLE = 'unknown';
|
|
13
|
+
/**
|
|
14
|
+
* 필요한 결정마다 주인이 하나로 정해지는지 본다 (C-04 §1.3).
|
|
15
|
+
*
|
|
16
|
+
* **결정 영역을 선언하지 않은 세션은 여기서 아무것도 걸리지 않는다.** 모든 세션이
|
|
17
|
+
* cross-part 결정을 요구하지는 않으므로, owner가 비었다는 이유만으로 발급을 막지 않는다.
|
|
18
|
+
* 막는 것은 "정해야 할 것이 있는데 정할 사람이 없거나 둘인" 경우뿐이다.
|
|
19
|
+
*
|
|
20
|
+
* 오류 문구가 다음 명령까지 말하는 이유: 무엇이 잘못됐는지만 알려 주면 처음 쓰는 사람은
|
|
21
|
+
* 여기서 멈춘다. 어떻게 푸는지가 같은 화면에 있어야 한다.
|
|
22
|
+
*/
|
|
23
|
+
function responsibilityFailures(spec, ownership) {
|
|
24
|
+
const failures = [];
|
|
25
|
+
for (const domain of spec.decisionDomains ?? []) {
|
|
26
|
+
if (spec.decisionAuthority?.[domain])
|
|
27
|
+
continue; // 이번 세션에 한해 사람이 정했다
|
|
28
|
+
const found = lookupAuthority(ownership, domain);
|
|
29
|
+
if (found.kind === 'RESOLVED')
|
|
30
|
+
continue;
|
|
31
|
+
const how = `--authority ${domain}=<role> 로 이번 세션의 결정권자를 정하거나, Profile ownership 에 적어라`;
|
|
32
|
+
failures.push({
|
|
33
|
+
kind: 'RESPONSIBILITY_AMBIGUOUS',
|
|
34
|
+
detail: found.kind === 'AMBIGUOUS'
|
|
35
|
+
? `'${domain}' 의 결정권자가 갈려 있다 (${found.candidates.join(', ')}) — ${how}`
|
|
36
|
+
: `'${domain}' 의 결정권자가 선언되지 않았다 — ${how}`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return failures;
|
|
40
|
+
}
|
|
41
|
+
export class SessionRuntime {
|
|
42
|
+
#store;
|
|
43
|
+
#policy;
|
|
44
|
+
#scm;
|
|
45
|
+
#canonicalSources;
|
|
46
|
+
#ownership;
|
|
47
|
+
#bindings;
|
|
48
|
+
constructor(store, policy = null, deps = {}) {
|
|
49
|
+
this.#store = store;
|
|
50
|
+
this.#policy = policy;
|
|
51
|
+
this.#scm = deps.scm;
|
|
52
|
+
this.#canonicalSources = deps.canonicalSources ?? [];
|
|
53
|
+
this.#ownership = deps.ownership;
|
|
54
|
+
this.#bindings = deps.bindings;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 이 Physical Run이 그 세션의 기록을 쓸 자격이 있는가 (C-10 §2.3 불변식 ⑤).
|
|
58
|
+
*
|
|
59
|
+
* 승계 후 죽지 않은 옛 Host가 계속 쓰면 기록이 오염된다. Progress는 이미 이 검사를
|
|
60
|
+
* 하고 있었고 checkpoint·handoff만 열려 있었다 — 그 비대칭을 여기서 닫는다.
|
|
61
|
+
*/
|
|
62
|
+
async #guardOwner(id, physicalSessionId) {
|
|
63
|
+
if (!this.#bindings)
|
|
64
|
+
return null;
|
|
65
|
+
const binding = await this.#bindings.get(id);
|
|
66
|
+
// binding이 없으면 소유권 개념이 없는 세션이다 — 막지 않는다
|
|
67
|
+
if (!binding)
|
|
68
|
+
return null;
|
|
69
|
+
if (!physicalSessionId) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
reason: 'NOT_OWNER',
|
|
73
|
+
detail: `${id} 에는 Runtime이 붙어 있다 — owner(${binding.physicalSessionId})만 기록할 수 있다`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (binding.physicalSessionId !== physicalSessionId) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
reason: 'NOT_OWNER',
|
|
80
|
+
detail: `${id} 의 owner가 아니다 (현재 owner: ${binding.physicalSessionId})`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 지금 정본이 어디에 있는지. source별로 따로 읽는다 — 하나로 뭉개지 않는다.
|
|
87
|
+
*
|
|
88
|
+
* Profile이 정본을 선언했는데 읽을 통로가 없으면 실패로 돌려준다. 빈 배열로 넘기면
|
|
89
|
+
* 이후 대조가 아무것도 비교하지 않고 통과해, 검증이 있는 척만 하는 상태가 된다.
|
|
90
|
+
*/
|
|
91
|
+
async #readBaselines() {
|
|
92
|
+
if (this.#canonicalSources.length === 0)
|
|
93
|
+
return { ok: true, snapshots: [] };
|
|
94
|
+
if (!this.#scm) {
|
|
95
|
+
const ids = this.#canonicalSources.map((s) => s.sourceId).join(', ');
|
|
96
|
+
return { ok: false, detail: `정본(${ids})을 읽을 통로가 없다 — 자격 증명을 확인하라` };
|
|
97
|
+
}
|
|
98
|
+
const snapshots = await this.#scm.getBaselines(this.#canonicalSources);
|
|
99
|
+
const unreadable = snapshots.filter((s) => s.baseline === UNREADABLE).map((s) => s.sourceId);
|
|
100
|
+
if (unreadable.length > 0)
|
|
101
|
+
return { ok: false, detail: `정본을 읽지 못했다: ${unreadable.join(', ')}` };
|
|
102
|
+
return { ok: true, snapshots };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 계약에 적힌 baseline과 지금을 견준다.
|
|
106
|
+
* 세션 파일의 요약이 아니라 정본을 실제로 다시 읽는다는 것이 이 함수의 존재 이유다 (OM §9).
|
|
107
|
+
*/
|
|
108
|
+
async #detectDrift(session) {
|
|
109
|
+
const recorded = session.canonicalSources ?? [];
|
|
110
|
+
if (recorded.length === 0)
|
|
111
|
+
return { ok: true, drifts: [] };
|
|
112
|
+
const current = await this.#readBaselines();
|
|
113
|
+
// 기록된 baseline이 있는데 지금 읽을 수 없다면, 같은지 다른지 말할 수 없다.
|
|
114
|
+
if (!current.ok)
|
|
115
|
+
return current;
|
|
116
|
+
const drifts = [];
|
|
117
|
+
for (const before of recorded) {
|
|
118
|
+
const now = current.snapshots.find((c) => c.sourceId === before.sourceId);
|
|
119
|
+
if (now && now.baseline !== before.baseline) {
|
|
120
|
+
drifts.push({ sourceId: before.sourceId, recorded: before.baseline, current: now.baseline });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { ok: true, drifts };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Controller가 세션 계약을 발급한다. Session Contract는 Policy hierarchy의 최하위
|
|
127
|
+
* 계층이므로 상위보다 넓은 권한을 요청할 수 없다 (OM §5.2).
|
|
128
|
+
* 범위를 벗어난 요청은 조용히 좁히지 않고 거절한다 — 계약서와 실권한이 어긋난 채로
|
|
129
|
+
* 굴러가는 것이 더 위험하다.
|
|
130
|
+
*/
|
|
131
|
+
async issue(spec) {
|
|
132
|
+
const failures = [];
|
|
133
|
+
if (this.#policy) {
|
|
134
|
+
const maxScope = this.#policy.roleScopes[spec.role];
|
|
135
|
+
for (const requested of spec.writeBoundary ?? []) {
|
|
136
|
+
if (parseScope(requested) === null) {
|
|
137
|
+
failures.push({ kind: 'INVALID_SCOPE', detail: `'${requested}' is not valid ASC scope grammar` });
|
|
138
|
+
}
|
|
139
|
+
else if (maxScope && !isWithinScopes(requested, maxScope)) {
|
|
140
|
+
failures.push({
|
|
141
|
+
kind: 'SCOPE_ESCALATION',
|
|
142
|
+
detail: `${spec.role} may not write '${requested}' — outside ${maxScope.join(', ')}`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Policy Exception은 SOFT DENY 전용이다. HARD DENY를 여기 적는 것은 해제 시도다.
|
|
147
|
+
for (const exception of spec.policyExceptions ?? []) {
|
|
148
|
+
if (this.#policy.hardDeny.includes(exception)) {
|
|
149
|
+
failures.push({
|
|
150
|
+
kind: 'HARD_DENY_ESCAPE',
|
|
151
|
+
detail: `'${exception}' is HARD DENY — a session contract cannot grant an exception for it`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
failures.push(...responsibilityFailures(spec, this.#ownership));
|
|
157
|
+
if (failures.length > 0)
|
|
158
|
+
return { ok: false, failures };
|
|
159
|
+
// 계약이 형식을 어기는 것도 발급 실패다. 예외로 새어 나가면 부르는 쪽마다
|
|
160
|
+
// 따로 감싸야 하고, CLI에서는 스택 트레이스가 사람에게 튄다.
|
|
161
|
+
// 발급 시점의 정본을 계약에 박아 둔다. 나중에 무엇을 딛고 시작했는지가 여기 남는다.
|
|
162
|
+
let canonicalSources = spec.canonicalSources;
|
|
163
|
+
if (!canonicalSources) {
|
|
164
|
+
const read = await this.#readBaselines();
|
|
165
|
+
if (!read.ok)
|
|
166
|
+
return { ok: false, failures: [{ kind: 'CANONICAL_UNAVAILABLE', detail: read.detail }] };
|
|
167
|
+
canonicalSources = read.snapshots;
|
|
168
|
+
}
|
|
169
|
+
const parsed = Session.safeParse({
|
|
170
|
+
...spec,
|
|
171
|
+
...(canonicalSources.length > 0 ? { canonicalSources } : {}),
|
|
172
|
+
version: 0,
|
|
173
|
+
status: 'READY',
|
|
174
|
+
});
|
|
175
|
+
if (!parsed.success) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
failures: parsed.error.issues.map((issue) => ({
|
|
179
|
+
kind: 'INVALID_CONTRACT',
|
|
180
|
+
detail: `${issue.path.join('.') || '(root)'}: ${issue.message}`,
|
|
181
|
+
})),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const session = parsed.data;
|
|
185
|
+
const created = await this.#store.create('session', session);
|
|
186
|
+
if (!created.ok) {
|
|
187
|
+
return { ok: false, failures: [{ kind: 'ALREADY_EXISTS', detail: `session '${spec.id}' already exists` }] };
|
|
188
|
+
}
|
|
189
|
+
return { ok: true, session: created.entity };
|
|
190
|
+
}
|
|
191
|
+
get(id) {
|
|
192
|
+
return this.#store.get('session', id);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Physical Run이 계약을 집어 든다. 집기 전에 정본이 그대로인지 본다 —
|
|
196
|
+
* 바닥이 달라진 채로 이어가면 낡은 전제 위에서 작업하게 된다.
|
|
197
|
+
*/
|
|
198
|
+
async start(id) {
|
|
199
|
+
const drift = await this.#guardCanonical(id);
|
|
200
|
+
if (drift)
|
|
201
|
+
return drift;
|
|
202
|
+
return applyTransition(this.#store, 'session', id, (s) => transitionSession(s, 'ACTIVE', 'session'));
|
|
203
|
+
}
|
|
204
|
+
/** Run이 끊긴다. Checkpoint 없이는 멈출 수 없다 — 다음 Run이 이어받을 수 없기 때문이다. */
|
|
205
|
+
async pause(id, checkpoint, physicalSessionId) {
|
|
206
|
+
const denied = await this.#guardOwner(id, physicalSessionId);
|
|
207
|
+
if (denied)
|
|
208
|
+
return denied;
|
|
209
|
+
return applyTransition(this.#store, 'session', id, (s) => transitionSession(s, 'PAUSED', 'session', { checkpoint }));
|
|
210
|
+
}
|
|
211
|
+
/** 다른 Physical Run이 같은 Logical Session을 이어받는다. 여기서도 정본부터 확인한다. */
|
|
212
|
+
async resume(id) {
|
|
213
|
+
const drift = await this.#guardCanonical(id);
|
|
214
|
+
if (drift)
|
|
215
|
+
return drift;
|
|
216
|
+
return applyTransition(this.#store, 'session', id, (s) => transitionSession(s, 'ACTIVE', 'session'));
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* 지금 정본 상태를 읽기 전용으로 판정한다 (C-03 §1.6). ACTIVE 세션을 이어갈 때도
|
|
220
|
+
* 검증이 필요하지만 상태 전이는 없어야 하므로, 판정 로직을 Operator에 복제하지 않고
|
|
221
|
+
* 여기서 공개한다.
|
|
222
|
+
*/
|
|
223
|
+
async checkCanonical(id) {
|
|
224
|
+
const session = await this.#store.get('session', id);
|
|
225
|
+
if (!session)
|
|
226
|
+
return { status: 'NOT_FOUND' };
|
|
227
|
+
const detected = await this.#detectDrift(session);
|
|
228
|
+
if (!detected.ok)
|
|
229
|
+
return { status: 'UNAVAILABLE', detail: detected.detail };
|
|
230
|
+
return detected.drifts.length > 0 ? { status: 'DRIFT', drifts: detected.drifts } : { status: 'CURRENT' };
|
|
231
|
+
}
|
|
232
|
+
async #guardCanonical(id) {
|
|
233
|
+
const session = await this.#store.get('session', id);
|
|
234
|
+
if (!session)
|
|
235
|
+
return null; // 없는 세션은 전이 단계에서 NOT_FOUND로 걸린다
|
|
236
|
+
const detected = await this.#detectDrift(session);
|
|
237
|
+
if (!detected.ok)
|
|
238
|
+
return { ok: false, reason: 'CANONICAL_UNAVAILABLE', detail: detected.detail };
|
|
239
|
+
return detected.drifts.length > 0 ? { ok: false, reason: 'CANONICAL_DRIFT', drifts: detected.drifts } : null;
|
|
240
|
+
}
|
|
241
|
+
/** HARD DENY가 Goal을 막았다 — Controller 판단이 필요하다 (OM §5.3). */
|
|
242
|
+
block(id) {
|
|
243
|
+
return applyTransition(this.#store, 'session', id, (s) => transitionSession(s, 'BLOCKED', 'session'));
|
|
244
|
+
}
|
|
245
|
+
/** 막힌 사유를 Controller가 해소했다. */
|
|
246
|
+
unblock(id) {
|
|
247
|
+
return applyTransition(this.#store, 'session', id, (s) => transitionSession(s, 'ACTIVE', 'controller'));
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* 세션 종료. Handoff까지가 에이전트 몫이고, state·block·queue 갱신은 Controller가
|
|
251
|
+
* 회수한 뒤에 한다 (OM §7.2·§9).
|
|
252
|
+
*/
|
|
253
|
+
async complete(id, handoff, physicalSessionId) {
|
|
254
|
+
const denied = await this.#guardOwner(id, physicalSessionId);
|
|
255
|
+
if (denied)
|
|
256
|
+
return denied;
|
|
257
|
+
// 끝낸 시점의 정본을 Handoff에 남긴다. 다음 세션이 무엇을 기준으로 이어받는지가 이 값이다.
|
|
258
|
+
// 여기서는 읽지 못해도 막지 않는다 — 종료를 막으면 Handoff를 남길 길이 없어져 세션이
|
|
259
|
+
// 갇힌다. 대신 다음 발급·시작이 같은 이유로 멈추므로 모르는 채 이어지지는 않는다.
|
|
260
|
+
let snapshot = handoff.snapshot;
|
|
261
|
+
if (snapshot.length === 0) {
|
|
262
|
+
const read = await this.#readBaselines();
|
|
263
|
+
if (read.ok)
|
|
264
|
+
snapshot = read.snapshots;
|
|
265
|
+
}
|
|
266
|
+
const outcome = await applyTransition(this.#store, 'session', id, (s) => transitionSession(s, 'DONE', 'session', { handoff: { ...handoff, snapshot } }));
|
|
267
|
+
if (outcome.ok) {
|
|
268
|
+
await this.#store.appendHistory({
|
|
269
|
+
at: handoff.recordedAt,
|
|
270
|
+
actor: id,
|
|
271
|
+
kind: 'session_handoff',
|
|
272
|
+
ref: id,
|
|
273
|
+
detail: handoff.next,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return outcome;
|
|
277
|
+
}
|
|
278
|
+
fail(id, actor = 'session') {
|
|
279
|
+
return applyTransition(this.#store, 'session', id, (s) => transitionSession(s, 'FAILED', actor));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TransitionError } from '../model/transitions.ts';
|
|
2
|
+
import type { EntityKind, EntityMap, StateStore } from '../../ports/state-store.ts';
|
|
3
|
+
export type ApplyOutcome<T> = {
|
|
4
|
+
ok: true;
|
|
5
|
+
entity: T;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
reason: 'NOT_FOUND';
|
|
9
|
+
}
|
|
10
|
+
/** 읽은 뒤 값이 바뀌었다. `current`가 호출자에게 무슨 일이 있었는지 설명할 근거다. */
|
|
11
|
+
| {
|
|
12
|
+
ok: false;
|
|
13
|
+
reason: 'CONFLICT';
|
|
14
|
+
current: T;
|
|
15
|
+
}
|
|
16
|
+
/** 전이 자체가 불가하다 — 잘못된 상태·권한 없는 actor·빠진 필수 항목. */
|
|
17
|
+
| {
|
|
18
|
+
ok: false;
|
|
19
|
+
reason: 'REJECTED';
|
|
20
|
+
failure: TransitionError;
|
|
21
|
+
};
|
|
22
|
+
export declare function applyTransition<K extends EntityKind>(store: StateStore, kind: K, id: string, mutate: (current: EntityMap[K]) => EntityMap[K]): Promise<ApplyOutcome<EntityMap[K]>>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// 읽기 → 전이 → CAS를 한 묶음으로 수행하는 공통 뼈대.
|
|
2
|
+
// Session·Request·Grant 오케스트레이션이 전부 이 위에 올라간다.
|
|
3
|
+
//
|
|
4
|
+
// 자동 재시도는 없다. 충돌은 "그 사이 누군가 결정했다"는 사실이고, 사람 판단이 걸린
|
|
5
|
+
// 전이를 몰래 다시 시도하면 이미 내려진 결정을 덮어쓴다 (C-01 §8).
|
|
6
|
+
import { TransitionError } from "../model/transitions.js";
|
|
7
|
+
export async function applyTransition(store, kind, id, mutate) {
|
|
8
|
+
const current = await store.get(kind, id);
|
|
9
|
+
if (!current)
|
|
10
|
+
return { ok: false, reason: 'NOT_FOUND' };
|
|
11
|
+
let next;
|
|
12
|
+
try {
|
|
13
|
+
next = mutate(current);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error instanceof TransitionError)
|
|
17
|
+
return { ok: false, reason: 'REJECTED', failure: error };
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
const result = await store.compareAndSet(kind, id, current.version, next);
|
|
21
|
+
if (result.ok)
|
|
22
|
+
return { ok: true, entity: result.entity };
|
|
23
|
+
if (result.reason === 'NOT_FOUND')
|
|
24
|
+
return { ok: false, reason: 'NOT_FOUND' };
|
|
25
|
+
return { ok: false, reason: 'CONFLICT', current: result.current };
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ApprovalRequest, ControlState } from '../model/entities.ts';
|
|
2
|
+
import type { ScmPort } from '../../ports/scm.ts';
|
|
3
|
+
import type { CurrentContextOverlay, DecisionSummary, DecisionView, Freshness, StoredPacket, VerificationCoverage } from './decision-view.ts';
|
|
4
|
+
/** 어느 채널에서도 이 문자열로 같은 요청을 지목할 수 있어야 한다 (C-01 §2). */
|
|
5
|
+
export declare function referenceOf(request: ApprovalRequest): string;
|
|
6
|
+
export declare function storedPacketOf(request: ApprovalRequest): StoredPacket;
|
|
7
|
+
export declare function summarize(request: ApprovalRequest, freshness: Freshness): DecisionSummary;
|
|
8
|
+
export type OverlayInput = {
|
|
9
|
+
control: ControlState;
|
|
10
|
+
observedAt: string;
|
|
11
|
+
/** 없으면 외부 상태를 확인할 수 없다 — canonical 비교와 스레드 확인을 건너뛴다. */
|
|
12
|
+
scm?: ScmPort;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* 조회 시점의 사실을 다시 읽어 만든 Derived View.
|
|
16
|
+
* 원본 요청도 History도 건드리지 않는다 (C-01 §6-B).
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildOverlay(request: ApprovalRequest, input: OverlayInput): Promise<CurrentContextOverlay>;
|
|
19
|
+
/**
|
|
20
|
+
* 조회 시점의 신선도와, 그 판단이 무엇을 근거로 했는지 (C-01 §7).
|
|
21
|
+
*
|
|
22
|
+
* `CURRENT`는 "아무것도 안 변했다"가 아니라 "확인할 수 있었던 범위에서 안 변했다"이다.
|
|
23
|
+
* 외부 연결이 없어 원본을 못 본 채 얻은 CURRENT와, 원본까지 확인하고 얻은 CURRENT는
|
|
24
|
+
* 사용자에게 다른 이야기이므로 verification으로 갈라 말한다.
|
|
25
|
+
*
|
|
26
|
+
* 더 무거운 사실이 가벼운 사실을 덮는다: 이미 결정됨 > 원본 변함 > 맥락 변함.
|
|
27
|
+
* 사용성·사전 경고용이며 게시 직전 Drift Guard를 대신하지 않는다 (OM §11.9).
|
|
28
|
+
*/
|
|
29
|
+
export declare function assess(request: ApprovalRequest, overlay: CurrentContextOverlay, scm?: ScmPort): Promise<{
|
|
30
|
+
freshness: Freshness;
|
|
31
|
+
verification: VerificationCoverage;
|
|
32
|
+
}>;
|
|
33
|
+
export declare function assembleView(request: ApprovalRequest, assessment: {
|
|
34
|
+
freshness: Freshness;
|
|
35
|
+
verification: VerificationCoverage;
|
|
36
|
+
}, overlay?: CurrentContextOverlay): DecisionView;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// ApprovalRequest → Shared Decision View Model 조립.
|
|
2
|
+
//
|
|
3
|
+
// 여기서 지키는 것 하나: 요청이 만들어질 때의 분석(stored)과 지금 다시 읽은 사실(current)을
|
|
4
|
+
// 절대 한 덩어리로 섞지 않는다 (C-01 §6). 14:00에 "S-020에 영향 없음"이라고 적힌 판단은
|
|
5
|
+
// 14:20에도 그때의 판단일 뿐이고, 지금 무슨 일이 벌어지는지는 따로 계산해 나란히 놓는다.
|
|
6
|
+
/** 어느 채널에서도 이 문자열로 같은 요청을 지목할 수 있어야 한다 (C-01 §2). */
|
|
7
|
+
export function referenceOf(request) {
|
|
8
|
+
return `ASC · ${request.priority} · ${request.id} · ${request.source.reference}`;
|
|
9
|
+
}
|
|
10
|
+
const DECIDED_STATUSES = new Set(['APPROVED', 'QUEUED', 'DEFERRED', 'DISMISSED', 'DONE']);
|
|
11
|
+
export function storedPacketOf(request) {
|
|
12
|
+
return {
|
|
13
|
+
status: request.status,
|
|
14
|
+
type: request.type,
|
|
15
|
+
priority: request.priority,
|
|
16
|
+
title: request.title,
|
|
17
|
+
detectedAt: request.detectedAt,
|
|
18
|
+
source: request.source.reference,
|
|
19
|
+
situation: request.situation,
|
|
20
|
+
context: request.context,
|
|
21
|
+
interruptRequired: request.impact.interruptRequired,
|
|
22
|
+
affectedSessions: request.impact.affectedSessions,
|
|
23
|
+
rationale: request.impact.rationale,
|
|
24
|
+
recommendation: request.recommendation,
|
|
25
|
+
...(request.draft !== undefined ? { draft: request.draft } : {}),
|
|
26
|
+
snapshot: request.snapshot,
|
|
27
|
+
...(request.source.threadLastEventId !== undefined
|
|
28
|
+
? { threadLastEventId: request.source.threadLastEventId }
|
|
29
|
+
: {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function summarize(request, freshness) {
|
|
33
|
+
return {
|
|
34
|
+
requestId: request.id,
|
|
35
|
+
reference: referenceOf(request),
|
|
36
|
+
version: request.version,
|
|
37
|
+
freshness,
|
|
38
|
+
status: request.status,
|
|
39
|
+
priority: request.priority,
|
|
40
|
+
title: request.title,
|
|
41
|
+
detectedAt: request.detectedAt,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 조회 시점의 사실을 다시 읽어 만든 Derived View.
|
|
46
|
+
* 원본 요청도 History도 건드리지 않는다 (C-01 §6-B).
|
|
47
|
+
*/
|
|
48
|
+
export async function buildOverlay(request, input) {
|
|
49
|
+
const notes = [];
|
|
50
|
+
const activeSessions = input.control.activeSessions;
|
|
51
|
+
// 생성 당시 "영향 있음"으로 지목된 세션과 지금 도는 세션이 겹치는가.
|
|
52
|
+
// 요청이 세션을 지목하지 않았다면 판단 근거가 없다 — 없음을 없음이라고 적는다.
|
|
53
|
+
const affected = request.impact.affectedSessions;
|
|
54
|
+
let affectsCurrentWork = false;
|
|
55
|
+
if (affected.length > 0) {
|
|
56
|
+
affectsCurrentWork = activeSessions.some((id) => affected.includes(id));
|
|
57
|
+
if (!affectsCurrentWork && activeSessions.length > 0) {
|
|
58
|
+
notes.push(`알림 당시 기준 세션(${affected.join(', ')})은 지금 활성이 아니다`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const canonicalChanges = [];
|
|
62
|
+
if (input.scm && request.snapshot.length > 0) {
|
|
63
|
+
const current = await input.scm.getBaselines(request.snapshot.map((s) => ({ sourceId: s.sourceId })));
|
|
64
|
+
for (const now of current) {
|
|
65
|
+
const before = request.snapshot.find((s) => s.sourceId === now.sourceId)?.baseline;
|
|
66
|
+
if (before !== now.baseline)
|
|
67
|
+
canonicalChanges.push({ sourceId: now.sourceId, before, after: now.baseline });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// 확인하지 못했다는 사실은 notes가 아니라 verification이 구조적으로 말한다 (assess 참조)
|
|
71
|
+
return { observedAt: input.observedAt, activeSessions, affectsCurrentWork, canonicalChanges, notes };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 조회 시점의 신선도와, 그 판단이 무엇을 근거로 했는지 (C-01 §7).
|
|
75
|
+
*
|
|
76
|
+
* `CURRENT`는 "아무것도 안 변했다"가 아니라 "확인할 수 있었던 범위에서 안 변했다"이다.
|
|
77
|
+
* 외부 연결이 없어 원본을 못 본 채 얻은 CURRENT와, 원본까지 확인하고 얻은 CURRENT는
|
|
78
|
+
* 사용자에게 다른 이야기이므로 verification으로 갈라 말한다.
|
|
79
|
+
*
|
|
80
|
+
* 더 무거운 사실이 가벼운 사실을 덮는다: 이미 결정됨 > 원본 변함 > 맥락 변함.
|
|
81
|
+
* 사용성·사전 경고용이며 게시 직전 Drift Guard를 대신하지 않는다 (OM §11.9).
|
|
82
|
+
*/
|
|
83
|
+
export async function assess(request, overlay, scm) {
|
|
84
|
+
const hasSourceToCheck = request.snapshot.length > 0 || request.source.threadLastEventId !== undefined;
|
|
85
|
+
const localContext = request.impact.affectedSessions.length > 0 ? 'VERIFIED' : 'NOT_APPLICABLE';
|
|
86
|
+
let source = 'NOT_APPLICABLE';
|
|
87
|
+
if (hasSourceToCheck)
|
|
88
|
+
source = scm ? 'VERIFIED' : 'UNAVAILABLE';
|
|
89
|
+
const verify = (freshness) => ({ freshness, verification: { localContext, source } });
|
|
90
|
+
if (DECIDED_STATUSES.has(request.status))
|
|
91
|
+
return verify('ALREADY_DECIDED');
|
|
92
|
+
if (overlay.canonicalChanges.length > 0)
|
|
93
|
+
return verify('SOURCE_CHANGED');
|
|
94
|
+
if (scm && request.source.threadLastEventId) {
|
|
95
|
+
const thread = await scm.getThread(request.source.reference);
|
|
96
|
+
if (thread.missing || thread.lastEventId !== request.source.threadLastEventId)
|
|
97
|
+
return verify('SOURCE_CHANGED');
|
|
98
|
+
}
|
|
99
|
+
// 알림 당시 지목된 세션이 더는 돌지 않거나 다른 세션이 도는 상황.
|
|
100
|
+
// 요청 자체는 그대로지만 사용자가 읽는 맥락이 달라졌다.
|
|
101
|
+
const affected = request.impact.affectedSessions;
|
|
102
|
+
if (affected.length > 0 && overlay.activeSessions.length > 0 && !overlay.affectsCurrentWork) {
|
|
103
|
+
return verify('STALE_CONTEXT');
|
|
104
|
+
}
|
|
105
|
+
return verify('CURRENT');
|
|
106
|
+
}
|
|
107
|
+
export function assembleView(request, assessment, overlay) {
|
|
108
|
+
return {
|
|
109
|
+
requestId: request.id,
|
|
110
|
+
reference: referenceOf(request),
|
|
111
|
+
version: request.version,
|
|
112
|
+
stored: storedPacketOf(request),
|
|
113
|
+
...(overlay ? { current: overlay } : {}),
|
|
114
|
+
freshness: assessment.freshness,
|
|
115
|
+
verification: assessment.verification,
|
|
116
|
+
allowedDecisions: request.allowedDecisions,
|
|
117
|
+
authorizedApprover: request.authorizedApprover,
|
|
118
|
+
...(request.expiresAt !== undefined ? { expiresAt: request.expiresAt } : {}),
|
|
119
|
+
...(request.decision
|
|
120
|
+
? {
|
|
121
|
+
decided: {
|
|
122
|
+
kind: request.decision.kind,
|
|
123
|
+
actor: request.decision.actor,
|
|
124
|
+
channel: request.decision.channel,
|
|
125
|
+
decidedAt: request.decision.decidedAt,
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
: {}),
|
|
129
|
+
...(request.resultRef !== undefined ? { resultRef: request.resultRef } : {}),
|
|
130
|
+
};
|
|
131
|
+
}
|