@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,454 @@
1
+ // Orchestration Audit — 무슨 일이 실제로 있었는가 (C-10).
2
+ //
3
+ // 두 사실을 따로 적는다. 합치면 둘 중 하나가 다른 하나의 증거인 척하게 된다:
4
+ //
5
+ // Delegation 누가 누구에게 무엇을 맡겼다고 **선언**했는가 (의도)
6
+ // Execution 실제로 어떤 실행 주체가 그것을 **집었는가** (사실)
7
+ //
8
+ // 위임만 있고 실행이 없으면 "발급됐으나 아무도 수행하지 않았다"이고, 그건 결함이 아니라
9
+ // 정확히 그 상태다 (C-10 §1.1). 그렇게 보이게 두는 것이 이 모듈의 목적이다.
10
+ //
11
+ // 왜 Progress가 아닌가 (C-10 §0.2): Progress는 살아 있는 표시라 최신 1건만 남고 회수 때
12
+ // 정리된다. 여기는 회수 후에도 남아야 하는 기록이다. 수명이 다르면 같은 자리에 두지 않는다.
13
+ //
14
+ // 왜 Session entity가 아닌가 (C-10 §0.1): OM §7.0 Entity 목록과 §11.2 상태 enum은 동결이다.
15
+ // Closure Ledger·Bounded Query·Observation Ledger와 같은 adapter-scope 레코드로 산다.
16
+ //
17
+ // 저장은 **전부 setIfAbsent 위에 선다.** ScopedStore가 원자성을 약속한 것은 그것뿐이고,
18
+ // 잃는 것이 표시값이 아니라 "그때 그 실행이 있었다"는 사실이기 때문이다. 그래서 바뀌는
19
+ // 값(실행 종료)도 갱신이 아니라 **끝 마커를 따로 append**해 읽을 때 합친다 —
20
+ // Closure Ledger가 항목별 확인 마커를 따로 두는 것과 같은 이유다.
21
+ import { z } from 'zod';
22
+ import { SessionId } from "../model/ids.js";
23
+ /**
24
+ * principal 신고 수준 (C-10 §3).
25
+ *
26
+ * ASC는 신고를 검증할 수단이 없다. Host attestation이 없는 상태에서 "검증했다"고 쓰는 것이
27
+ * 검증하지 않는 것보다 나쁘므로, 검증 대신 **신고 수준을 기록**한다.
28
+ */
29
+ export const PrincipalSource = z.enum(['declared', 'derived']);
30
+ export const ExecutionStatus = z.enum(['RUNNING', 'RELEASED', 'SUPERSEDED']);
31
+ export const DelegationRecord = z.object({
32
+ delegationId: z.string().min(1),
33
+ /** 위임한 세션. 없으면 최상위 발급이다 — 없는 부모를 지어내지 않는다. */
34
+ parentSessionId: SessionId.optional(),
35
+ childSessionId: SessionId,
36
+ role: z.string().min(1),
37
+ goal: z.string().min(1),
38
+ /** 세션 계약의 사본이 아니라 발급 시점 요약. 정본은 Session entity다. */
39
+ scope: z.array(z.string()).default([]),
40
+ doneCriteria: z.array(z.string()).default([]),
41
+ issuedBy: z.string().min(1),
42
+ issuedAt: z.string().min(1),
43
+ /** 결과를 누구에게 돌려야 하는가. 보통 parentSessionId. */
44
+ expectedReturnTo: z.string().optional(),
45
+ });
46
+ export const ExecutionEvidence = z.object({
47
+ executionId: z.string().min(1),
48
+ logicalSessionId: SessionId,
49
+ /** 어느 Host가 관찰했는가. provider 이름이 아니라 adapter id다. */
50
+ hostAdapter: z.string().min(1),
51
+ principal: z.string().min(1),
52
+ principalSource: PrincipalSource,
53
+ /** Host가 아는 실행 참조(physical session id 등). 같은 사람이 여럿 가질 수 있다. */
54
+ physicalReference: z.string().min(1),
55
+ startedAt: z.string().min(1),
56
+ finishedAt: z.string().optional(),
57
+ status: ExecutionStatus,
58
+ /** 이 사실이 어디서 왔는가 — 선언인가 관찰인가. */
59
+ evidenceSource: z.string().min(1),
60
+ });
61
+ /** 저장형. status·finishedAt은 여기 없다 — 끝 마커가 따로 산다. */
62
+ const StoredExecution = ExecutionEvidence.omit({ status: true, finishedAt: true });
63
+ const ExecutionEnd = z.object({
64
+ finishedAt: z.string().min(1),
65
+ status: z.enum(['RELEASED', 'SUPERSEDED']),
66
+ });
67
+ export const ReclaimEvidence = z.object({
68
+ sessionId: SessionId,
69
+ /** 회수 주체. 모르면 기록하지 않는다 — 익명 회수는 감사 공백이다 (C-10 §2.4 불변식 ⑥). */
70
+ reclaimedBy: z.string().min(1),
71
+ reclaimedAt: z.string().min(1),
72
+ /** 무엇을 받았는가. Handoff 시각으로 가리킨다 — 본문을 복제하지 않는다. */
73
+ handoffRef: z.string().optional(),
74
+ /** 회수 시점에 살아 있던 실행들. archive 뒤에는 세션에서 복원되지 않는다. */
75
+ executionRefs: z.array(z.string()).default([]),
76
+ });
77
+ /**
78
+ * 독립성 등급 (C-10 §4.2).
79
+ *
80
+ * ASC는 principal 신고를 검증하지 못한다. 그래서 "다르다"고 말할 수 있는 조건을
81
+ * 좁게 잡는다 — 양쪽 모두 선언됐고 실제로 다를 때만 독립이다.
82
+ */
83
+ export const Independence = z.enum(['INDEPENDENT', 'SELF_REPORTED', 'UNVERIFIED']);
84
+ export const ValidationRecord = z.object({
85
+ validationId: z.string().min(1),
86
+ validatorSessionId: SessionId,
87
+ /** 그 검증을 실제로 수행한 실행. 없으면 검증 세션이 있다는 주장뿐이다. */
88
+ validatorExecutionId: z.string().min(1),
89
+ principal: z.string().min(1),
90
+ principalSource: PrincipalSource,
91
+ targetSessionId: SessionId,
92
+ /** 무엇을 보고 판정했는가. Handoff 시각·리비전으로 가리킨다. */
93
+ targetHandoffRef: z.string().optional(),
94
+ targetRevision: z.string().optional(),
95
+ result: z.enum(['PASS', 'FAIL']),
96
+ /** 무엇을 봤는가. 결과만 남기면 다음 사람이 다시 볼 수 없다. */
97
+ findings: z.array(z.string()).default([]),
98
+ verifiedAt: z.string().min(1),
99
+ independence: Independence,
100
+ /** 왜 그 등급인가. 등급만 있으면 뒤집을 근거가 없다. */
101
+ independenceDetail: z.string().min(1),
102
+ });
103
+ /**
104
+ * 승인 없이 내린 결정의 갈래 (C-13 §4.1).
105
+ *
106
+ * 앞 넷은 자율 판단의 자리이고, 뒤 다섯은 escalation predicate와 짝을 이룬다 —
107
+ * 뒤쪽 class로 결정을 내렸는데 상신이 없으면 그 자체가 이상 신호다.
108
+ */
109
+ export const DecisionClass = z.enum([
110
+ 'implementation_detail',
111
+ 'owned_contract_consumption',
112
+ 'local_test_strategy',
113
+ 'local_refactor',
114
+ 'external_boundary',
115
+ 'shared_contract',
116
+ 'acceptance',
117
+ 'permission',
118
+ 'irreversible',
119
+ ]);
120
+ /**
121
+ * 승인 없이 갔다는 것이 기록이 없어도 된다는 뜻은 아니다 (C-13 §4).
122
+ *
123
+ * **부여받은 authority 안에서의 자율 판단임을 감사 가능하게** 남긴다. 무엇과 견줬는지
124
+ * (alternatives)가 없으면 비교 없이 고른 것과 구분되지 않는다.
125
+ */
126
+ export const DecisionEvidence = z.object({
127
+ decisionId: z.string().min(1),
128
+ sessionId: SessionId,
129
+ actor: z.string().min(1),
130
+ /** 어느 권한 안에서 내렸는가. 비어 있으면 권한을 대지 못한 결정이다. */
131
+ ownership: z.array(z.string()).default([]),
132
+ class: DecisionClass,
133
+ evidenceRefs: z.array(z.string().min(1)).nonempty(),
134
+ selectedOption: z.string().min(1),
135
+ alternatives: z.array(z.string()).default([]),
136
+ /** 왜 경계가 아니었는가. 이것이 없으면 자율 판단이 아니라 그냥 안 물어본 것이다. */
137
+ whyNoApproval: z.array(z.string().min(1)).nonempty(),
138
+ verification: z.array(z.string()).default([]),
139
+ decidedAt: z.string().min(1),
140
+ });
141
+ /** 한 child session은 한 번 위임된다 — 그래서 id 자체가 키다. */
142
+ const delegationKey = (childSessionId) => `audit:del:${childSessionId}`;
143
+ const delegationPrefix = 'audit:del:';
144
+ const executionKey = (sessionId, seq) => `audit:exec:${sessionId}:${seq}`;
145
+ const executionPrefix = (sessionId) => sessionId ? `audit:exec:${sessionId}:` : 'audit:exec:';
146
+ const executionEndKey = (executionId) => `audit:exec-end:${executionId}`;
147
+ const reclaimKey = (sessionId) => `audit:rec:${sessionId}`;
148
+ const decisionKey = (sessionId, seq) => `audit:dec:${sessionId}:${seq}`;
149
+ const decisionPrefix = (sessionId) => `audit:dec:${sessionId}:`;
150
+ const validationKey = (targetSessionId, seq) => `audit:val:${targetSessionId}:${seq}`;
151
+ const validationPrefix = (targetSessionId) => `audit:val:${targetSessionId}:`;
152
+ /** `E-S-20260826-01-2` — 실행 참조가 아니라 이 기록의 이름이다. */
153
+ const executionIdOf = (sessionId, seq) => `E-${sessionId}-${seq}`;
154
+ const seqOfKey = (key) => Number(key.slice(key.lastIndexOf(':') + 1));
155
+ export class AuditLedger {
156
+ #scope;
157
+ #now;
158
+ constructor(scope, now = () => new Date().toISOString()) {
159
+ this.#scope = scope;
160
+ this.#now = now;
161
+ }
162
+ /**
163
+ * 위임을 선언으로 기록한다.
164
+ *
165
+ * **상태 전이를 일으키지 않는다** (C-10 §1.2 불변식 ②). 전이는 전이표만 한다.
166
+ * 두 번째 위임 선언은 조용히 덮지 않고 거부한다 — 같은 세션을 두 번 맡겼다면
167
+ * 그건 기록이 아니라 사고이므로 사람이 봐야 한다.
168
+ */
169
+ async delegate(input) {
170
+ const record = DelegationRecord.parse({
171
+ ...input,
172
+ delegationId: `D-${input.childSessionId}`,
173
+ issuedAt: input.issuedAt ?? this.#now(),
174
+ });
175
+ const key = delegationKey(record.childSessionId);
176
+ if (await this.#scope.setIfAbsent(key, JSON.stringify(record)))
177
+ return { ok: true, record };
178
+ const existing = await this.#read(key, DelegationRecord);
179
+ return {
180
+ ok: false,
181
+ reason: 'ALREADY_RECORDED',
182
+ detail: `${record.childSessionId} 는 이미 위임 기록이 있다`,
183
+ ...(existing ? { existing } : { existing: record }),
184
+ };
185
+ }
186
+ async delegationOf(childSessionId) {
187
+ return this.#read(delegationKey(childSessionId), DelegationRecord);
188
+ }
189
+ /** 이 세션이 위임한 것들. parent 축으로 본다. */
190
+ async delegationsFrom(parentSessionId) {
191
+ const all = await this.#list(delegationPrefix, DelegationRecord);
192
+ return all.filter((d) => d.parentSessionId === parentSessionId);
193
+ }
194
+ /**
195
+ * 실행이 실제로 시작됐다는 사실을 남긴다.
196
+ *
197
+ * 순번은 setIfAbsent가 성공할 때까지 올린다 — 읽고-고쳐-쓰기를 하면 두 실행이 동시에
198
+ * 붙을 때 하나가 조용히 사라진다. 실패는 경쟁이지 오류가 아니므로 다음 번호로 간다.
199
+ */
200
+ async execute(input) {
201
+ const startedAt = input.startedAt ?? this.#now();
202
+ const existing = await this.#scope.keys(executionPrefix(input.logicalSessionId));
203
+ let seq = existing.reduce((max, key) => Math.max(max, seqOfKey(key) || 0), 0) + 1;
204
+ for (;;) {
205
+ const stored = StoredExecution.parse({
206
+ ...input,
207
+ executionId: executionIdOf(input.logicalSessionId, seq),
208
+ startedAt,
209
+ });
210
+ if (await this.#scope.setIfAbsent(executionKey(input.logicalSessionId, seq), JSON.stringify(stored))) {
211
+ return { ok: true, evidence: { ...stored, status: 'RUNNING' } };
212
+ }
213
+ seq += 1;
214
+ }
215
+ }
216
+ /**
217
+ * 실행이 끝났다. **시작 기록을 고치지 않고 끝 마커를 따로 남긴다** —
218
+ * 현재 소유권(Runtime Binding)은 사라져도 "그때 그 실행이 있었다"는 사실은 남아야 한다
219
+ * (C-10 §1.3 불변식 ④).
220
+ */
221
+ async endExecution(executionId, status, at) {
222
+ const found = await this.#findExecution(executionId);
223
+ if (!found)
224
+ return { ok: false, reason: 'NOT_FOUND', detail: `${executionId} 실행 기록이 없다` };
225
+ const end = ExecutionEnd.parse({ finishedAt: at ?? this.#now(), status });
226
+ if (!(await this.#scope.setIfAbsent(executionEndKey(executionId), JSON.stringify(end)))) {
227
+ return { ok: false, reason: 'ALREADY_ENDED', detail: `${executionId} 는 이미 끝났다고 기록돼 있다` };
228
+ }
229
+ return { ok: true, evidence: { ...found, ...end } };
230
+ }
231
+ /** 이 세션을 거쳐 간 실행 전부. 순서는 순번 순 — 회수 후에도 남는다. */
232
+ async executionsOf(logicalSessionId) {
233
+ const keys = (await this.#scope.keys(executionPrefix(logicalSessionId))).sort((a, b) => (seqOfKey(a) || 0) - (seqOfKey(b) || 0));
234
+ const out = [];
235
+ for (const key of keys) {
236
+ const stored = await this.#read(key, StoredExecution);
237
+ if (stored)
238
+ out.push(await this.#compose(stored));
239
+ }
240
+ return out;
241
+ }
242
+ /**
243
+ * Controller가 실제로 회수했다는 별도 증거.
244
+ *
245
+ * 회수 시점에 살아 있던 실행을 함께 붙인다 — archive 이후에는 세션에서 그것을
246
+ * 되찾을 수 없기 때문이다 (C-10 §2.4).
247
+ */
248
+ async reclaim(input) {
249
+ const running = (await this.executionsOf(input.sessionId)).filter((e) => e.status === 'RUNNING');
250
+ const record = ReclaimEvidence.parse({ ...input, executionRefs: running.map((e) => e.executionId) });
251
+ if (await this.#scope.setIfAbsent(reclaimKey(record.sessionId), JSON.stringify(record))) {
252
+ return { ok: true, record };
253
+ }
254
+ return { ok: false, reason: 'ALREADY_RECLAIMED', detail: `${record.sessionId} 는 이미 회수 기록이 있다` };
255
+ }
256
+ async reclaimOf(sessionId) {
257
+ return this.#read(reclaimKey(sessionId), ReclaimEvidence);
258
+ }
259
+ /**
260
+ * 검증 결과를 기록한다.
261
+ *
262
+ * **검증자의 실행 증거가 없으면 기록하지 않는다** (C-10 §4). 세션 id만 만들어 두고
263
+ * "검증했다"고 적는 것이 정확히 이 계약이 막는 것이다.
264
+ *
265
+ * 독립성은 주장이 아니라 판정이다 — 여기서 계산해 넣고, 호출자가 정하지 못한다.
266
+ */
267
+ async validate(input) {
268
+ const validatorRuns = await this.executionsOf(input.validatorSessionId);
269
+ const run = validatorRuns.at(-1);
270
+ if (!run) {
271
+ return {
272
+ ok: false,
273
+ reason: 'NO_VALIDATOR_EXECUTION',
274
+ detail: `${input.validatorSessionId} 에 실행 증거가 없다 — 세션 id만으로는 검증을 기록하지 않는다`,
275
+ };
276
+ }
277
+ const judged = judgeIndependence(run, await this.executionsOf(input.targetSessionId));
278
+ const existing = await this.#scope.keys(validationPrefix(input.targetSessionId));
279
+ let seq = existing.reduce((max, key) => Math.max(max, seqOfKey(key) || 0), 0) + 1;
280
+ for (;;) {
281
+ const record = ValidationRecord.parse({
282
+ validationId: `V-${input.targetSessionId}-${seq}`,
283
+ validatorSessionId: input.validatorSessionId,
284
+ validatorExecutionId: run.executionId,
285
+ principal: run.principal,
286
+ principalSource: run.principalSource,
287
+ targetSessionId: input.targetSessionId,
288
+ ...(input.targetHandoffRef ? { targetHandoffRef: input.targetHandoffRef } : {}),
289
+ ...(input.targetRevision ? { targetRevision: input.targetRevision } : {}),
290
+ result: input.result,
291
+ findings: input.findings ?? [],
292
+ verifiedAt: input.verifiedAt ?? this.#now(),
293
+ independence: judged.independence,
294
+ independenceDetail: judged.detail,
295
+ });
296
+ if (await this.#scope.setIfAbsent(validationKey(input.targetSessionId, seq), JSON.stringify(record))) {
297
+ return { ok: true, record };
298
+ }
299
+ seq += 1;
300
+ }
301
+ }
302
+ /** 이 세션에 대한 검증들. 없으면 빈 배열 — "검증 없음"도 사실이다. */
303
+ async validationsOf(targetSessionId) {
304
+ const keys = (await this.#scope.keys(validationPrefix(targetSessionId))).sort((a, b) => (seqOfKey(a) || 0) - (seqOfKey(b) || 0));
305
+ const out = [];
306
+ for (const key of keys) {
307
+ const record = await this.#read(key, ValidationRecord);
308
+ if (record)
309
+ out.push(record);
310
+ }
311
+ return out;
312
+ }
313
+ /**
314
+ * 승인 없이 내린 결정을 남긴다.
315
+ *
316
+ * 순번은 setIfAbsent가 성공할 때까지 올린다 — 같은 세션의 두 결정이 겹쳐도 하나가
317
+ * 조용히 사라지지 않는다 (execute와 같은 형태).
318
+ */
319
+ async decide(input) {
320
+ const decidedAt = input.decidedAt ?? this.#now();
321
+ const existing = await this.#scope.keys(decisionPrefix(input.sessionId));
322
+ let seq = existing.reduce((max, key) => Math.max(max, seqOfKey(key) || 0), 0) + 1;
323
+ for (;;) {
324
+ const decision = DecisionEvidence.parse({
325
+ ...input,
326
+ decisionId: `D-${input.sessionId}-${seq}`,
327
+ decidedAt,
328
+ });
329
+ if (await this.#scope.setIfAbsent(decisionKey(input.sessionId, seq), JSON.stringify(decision))) {
330
+ return { ok: true, decision };
331
+ }
332
+ seq += 1;
333
+ }
334
+ }
335
+ /** 이 세션이 스스로 정한 것들. 승인 기록과 나란히 놓고 보는 것이 목적이다. */
336
+ async decisionsOf(sessionId) {
337
+ const keys = (await this.#scope.keys(decisionPrefix(sessionId))).sort((a, b) => (seqOfKey(a) || 0) - (seqOfKey(b) || 0));
338
+ const out = [];
339
+ for (const key of keys) {
340
+ const decision = await this.#read(key, DecisionEvidence);
341
+ if (decision)
342
+ out.push(decision);
343
+ }
344
+ return out;
345
+ }
346
+ async #findExecution(executionId) {
347
+ for (const key of await this.#scope.keys(executionPrefix())) {
348
+ const stored = await this.#read(key, StoredExecution);
349
+ if (stored?.executionId === executionId)
350
+ return stored;
351
+ }
352
+ return null;
353
+ }
354
+ async #compose(stored) {
355
+ const end = await this.#read(executionEndKey(stored.executionId), ExecutionEnd);
356
+ return end ? { ...stored, ...end } : { ...stored, status: 'RUNNING' };
357
+ }
358
+ async #read(key, schema) {
359
+ const raw = await this.#scope.get(key);
360
+ if (!raw)
361
+ return null;
362
+ const parsed = schema.safeParse(JSON.parse(raw));
363
+ // 깨진 기록은 판단 근거가 못 된다 — 그 항목만 건너뛴다 (guard·closure와 같은 태도)
364
+ return parsed.success ? parsed.data : null;
365
+ }
366
+ async #list(prefix, schema) {
367
+ const out = [];
368
+ for (const key of (await this.#scope.keys(prefix)).sort()) {
369
+ const record = await this.#read(key, schema);
370
+ if (record)
371
+ out.push(record);
372
+ }
373
+ return out;
374
+ }
375
+ }
376
+ /**
377
+ * 사람이 읽는 줄. **없는 단계를 비워 두지 않고 없다고 적는다** (C-10 §5 불변식 ⑩).
378
+ * "실행 증거 없음"은 감춰야 할 결함이 아니라 정상 출력이다.
379
+ */
380
+ export function executionLines(evidence) {
381
+ if (evidence.length === 0)
382
+ return [' 실행 증거 없음 — 발급됐으나 아무도 집지 않았다'];
383
+ return evidence.map((e) => {
384
+ const span = e.finishedAt ? `${e.startedAt} ~ ${e.finishedAt}` : `${e.startedAt} ~ (진행 중)`;
385
+ const source = e.principalSource === 'declared' ? '선언' : '유추';
386
+ return ` ${e.executionId} · ${e.principal}(${source}) · ${e.hostAdapter} · ${e.status} · ${span}`;
387
+ });
388
+ }
389
+ /**
390
+ * 검증자와 피검증자가 실제로 다른 주체인가 (C-10 §4.2).
391
+ *
392
+ * 대상 쪽 실행이 하나도 없으면 비교할 상대가 없다 — 그것도 UNVERIFIED다.
393
+ * "다른 실행이 없으니 독립이다"는 가장 위험한 오판이다.
394
+ */
395
+ export function judgeIndependence(validator, targetRuns) {
396
+ const others = targetRuns.filter((e) => e.executionId !== validator.executionId);
397
+ if (others.length === 0) {
398
+ return { independence: 'UNVERIFIED', detail: '대상 세션에 실행 증거가 없어 비교할 주체가 없다' };
399
+ }
400
+ if (others.some((e) => e.principal === validator.principal)) {
401
+ return {
402
+ independence: 'SELF_REPORTED',
403
+ detail: `검증자와 대상이 같은 주체다 (${validator.principal})`,
404
+ };
405
+ }
406
+ const derived = [validator, ...others].filter((e) => e.principalSource === 'derived');
407
+ if (derived.length > 0) {
408
+ return {
409
+ independence: 'UNVERIFIED',
410
+ detail: `주체가 선언되지 않아 다르다고 말할 근거가 없다 (${derived.map((e) => e.executionId).join(', ')})`,
411
+ };
412
+ }
413
+ return {
414
+ independence: 'INDEPENDENT',
415
+ detail: `검증자 ${validator.principal} 와 대상 ${others.map((e) => e.principal).join(', ')} 가 다른 선언 주체다`,
416
+ };
417
+ }
418
+ /** 사람이 읽는 검증 줄. **등급만 적고 이유를 빼지 않는다.** */
419
+ export function validationLines(records) {
420
+ if (records.length === 0)
421
+ return [' 검증 없음'];
422
+ return records.map((v) => ` ${v.validationId} · ${v.result} · ${v.independence} (${v.independenceDetail})` +
423
+ `
424
+ 검증자 ${v.validatorSessionId}/${v.validatorExecutionId} · ${v.verifiedAt}` +
425
+ (v.findings.length > 0 ? `
426
+ 확인: ${v.findings.join(' · ')}` : ''));
427
+ }
428
+ /**
429
+ * 사람이 읽는 결정 줄. **왜 승인이 필요 없었는지가 함께 온다** — 그 이유가 빠지면
430
+ * "그냥 안 물어봤다"와 구분되지 않는다.
431
+ */
432
+ export function decisionLines(decisions) {
433
+ if (decisions.length === 0)
434
+ return [' 자율 결정 기록 없음'];
435
+ return decisions.map((decision) => {
436
+ const alternatives = decision.alternatives.length > 0 ? ` (견준 것: ${decision.alternatives.join(', ')})` : '';
437
+ return (` ${decision.decisionId} [${decision.class}] ${decision.selectedOption}${alternatives}` +
438
+ `\n 권한: ${decision.ownership.join(', ') || '(미기재)'}` +
439
+ `\n 승인 불필요 근거: ${decision.whyNoApproval.join(' · ')}` +
440
+ `\n 확인: ${decision.verification.join(' · ') || '(없음)'}`);
441
+ });
442
+ }
443
+ export function reclaimLine(record, sessionId) {
444
+ if (!record)
445
+ return `${sessionId} — 회수 기록 없음`;
446
+ const refs = record.executionRefs.length > 0 ? ` · 회수 시점 실행 ${record.executionRefs.join(', ')}` : '';
447
+ return `${sessionId} 회수: ${record.reclaimedBy} ${record.reclaimedAt}${refs}`;
448
+ }
449
+ export function delegationLine(record, childSessionId) {
450
+ if (!record)
451
+ return `${childSessionId} — 위임 기록 없음 (최상위 발급이거나 기록되지 않았다)`;
452
+ const from = record.parentSessionId ?? '(최상위)';
453
+ return `${from} → ${record.childSessionId} [${record.role}] ${record.goal} · 발급 ${record.issuedBy} ${record.issuedAt}`;
454
+ }
@@ -0,0 +1,115 @@
1
+ import { z } from 'zod';
2
+ import type { ScopedStore } from '../../ports/state-store.ts';
3
+ /**
4
+ * 이 진술을 무엇으로 보는가 (C-10 §6).
5
+ *
6
+ * CONFIRMED 실측했다
7
+ * INFERRED 근거로부터 추론했다
8
+ * PENDING 확인이 필요하다
9
+ * STALE 나중 증거가 뒤집었다
10
+ */
11
+ export declare const ClaimStatus: z.ZodEnum<["CONFIRMED", "INFERRED", "PENDING", "STALE"]>;
12
+ export type ClaimStatus = z.infer<typeof ClaimStatus>;
13
+ /** id 문법. 파일명 변환이 단사가 아니라 자유 문자열을 그대로 키에 넣지 않는다. */
14
+ export declare const CLAIM_ID: RegExp;
15
+ export declare const Claim: z.ZodObject<{
16
+ claimId: z.ZodString;
17
+ /** 무엇을 주장하는가. 한 문장이어야 뒤집을 때 무엇이 뒤집혔는지 분명하다. */
18
+ statement: z.ZodString;
19
+ status: z.ZodEnum<["CONFIRMED", "INFERRED", "PENDING", "STALE"]>;
20
+ /** 어디서 왔는가 — 명령·문서·사람. 없으면 근거 없는 주장이다. */
21
+ evidenceRefs: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
22
+ observedAt: z.ZodString;
23
+ /** 이 claim이 대체한 것. */
24
+ supersedes: z.ZodOptional<z.ZodString>;
25
+ /** 이 claim을 대체한 것. **기록에 남기되 원본은 지우지 않는다.** */
26
+ supersededBy: z.ZodOptional<z.ZodString>;
27
+ /** 왜 뒤집혔는가. STALE인데 이유가 없으면 다음 사람이 같은 판단을 또 한다. */
28
+ supersededReason: z.ZodOptional<z.ZodString>;
29
+ }, "strip", z.ZodTypeAny, {
30
+ status: "STALE" | "CONFIRMED" | "INFERRED" | "PENDING";
31
+ evidenceRefs: string[];
32
+ observedAt: string;
33
+ claimId: string;
34
+ statement: string;
35
+ supersedes?: string | undefined;
36
+ supersededBy?: string | undefined;
37
+ supersededReason?: string | undefined;
38
+ }, {
39
+ status: "STALE" | "CONFIRMED" | "INFERRED" | "PENDING";
40
+ observedAt: string;
41
+ claimId: string;
42
+ statement: string;
43
+ evidenceRefs?: string[] | undefined;
44
+ supersedes?: string | undefined;
45
+ supersededBy?: string | undefined;
46
+ supersededReason?: string | undefined;
47
+ }>;
48
+ export type Claim = z.infer<typeof Claim>;
49
+ export type RecordOutcome = {
50
+ ok: true;
51
+ claim: Claim;
52
+ } | {
53
+ ok: false;
54
+ reason: 'DUPLICATE_ID';
55
+ detail: string;
56
+ } | {
57
+ ok: false;
58
+ reason: 'INVALID_ID';
59
+ detail: string;
60
+ };
61
+ export type SupersedeOutcome = {
62
+ ok: true;
63
+ stale: Claim;
64
+ current: Claim;
65
+ } | {
66
+ ok: false;
67
+ reason: 'NOT_FOUND' | 'ALREADY_STALE';
68
+ detail: string;
69
+ };
70
+ export declare class ClaimLedger {
71
+ #private;
72
+ constructor(scope: ScopedStore, now?: () => string);
73
+ /**
74
+ * 새 판단을 적는다. **같은 id를 덮어쓰지 않는다** — 덮어쓰면 그게 곧 "다시 쓰기"이고,
75
+ * 이 모듈이 막으려는 것이 정확히 그것이다.
76
+ */
77
+ record(input: {
78
+ claimId: string;
79
+ statement: string;
80
+ status: ClaimStatus;
81
+ evidenceRefs?: readonly string[];
82
+ observedAt?: string;
83
+ supersedes?: string;
84
+ }): Promise<RecordOutcome>;
85
+ /**
86
+ * 새 증거가 옛 판단을 뒤집었다.
87
+ *
88
+ * **옛 claim을 지우지도 고치지도 않는다.** 뒤집힘 마커를 따로 남기고, 읽을 때 합친다.
89
+ * 그래야 "그때는 왜 그렇게 봤는가"가 남는다.
90
+ */
91
+ supersede(input: {
92
+ staleId: string;
93
+ replacement: {
94
+ claimId: string;
95
+ statement: string;
96
+ status: ClaimStatus;
97
+ evidenceRefs?: readonly string[];
98
+ };
99
+ reason: string;
100
+ at?: string;
101
+ }): Promise<SupersedeOutcome>;
102
+ /** 당시 판단 그대로. 뒤집힌 것도 든다 — History는 지우지 않는다. */
103
+ history(): Promise<Claim[]>;
104
+ /**
105
+ * 지금 무엇이 사실로 서 있는가. **STALE을 뺀 최신 claim의 projection이며 저장하지 않는다**
106
+ * (C-10 불변식 ⑫).
107
+ */
108
+ current(): Promise<Claim[]>;
109
+ get(claimId: string): Promise<Claim | null>;
110
+ }
111
+ /**
112
+ * 사람이 읽는 줄. **추론을 확인처럼 보이게 하지 않는다** — 이 표시가 흐려지는 순간
113
+ * 사람이 추론 위에서 결정한다.
114
+ */
115
+ export declare function claimLines(claims: readonly Claim[]): string[];