@asc-agent/runtime 0.7.1 → 0.8.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.
@@ -73,15 +73,15 @@ export async function planBootstrap(input) {
73
73
  ? '쓸 수 있는 Profile이 없다 — 설치 경로에 profiles/<id>/profile.json 이 있어야 한다'
74
74
  : `어떤 Profile로 붙일지 (후보: ${candidates.join(', ')})`);
75
75
  if (candidates.length > 0)
76
- steps.push('asc init --profile <위 후보 중 하나>');
76
+ steps.push('asc setup --profile <위 후보 중 하나>');
77
77
  }
78
78
  else if (profile.kind === 'ATTACHED_UNKNOWN') {
79
79
  // 통과시키면 무엇으로 도는지 모르는 채 굴러간다 — bootstrapGuard가 막는 것과 같은 상태다
80
80
  undecided.push('.asc/ 는 있는데 profile.lock 을 읽지 못했다 — 붙이다 만 상태다');
81
- steps.push('asc init --profile <id> 로 다시 붙이거나 asc profile resolve --write 로 고정한다');
81
+ steps.push('asc setup 으로 다시 붙이거나 asc profile resolve --write 로 고정한다');
82
82
  }
83
83
  else if (!attached) {
84
- steps.push(`asc init --profile ${profile.id}`);
84
+ steps.push(`asc setup --profile ${profile.id}`);
85
85
  }
86
86
  for (const host of hosts) {
87
87
  if (!host.installed)
@@ -101,7 +101,7 @@ export async function planBootstrap(input) {
101
101
  undecided.push(policy.question);
102
102
  }
103
103
  }
104
- steps.push('asc setup status');
104
+ steps.push('asc status');
105
105
  if (attached)
106
106
  steps.push('asc proceed');
107
107
  return {
@@ -15,12 +15,12 @@
15
15
  * 포함한다 — "attach가 끝나야 아무것도 할 수 있다"는 오해를 이 목록이 직접 깬다.
16
16
  */
17
17
  const ALWAYS_READY = [
18
- 'issue, run, pause, resume and finish sessions (asc session, asc proceed)',
19
- 'record and read progress (asc progress)',
20
- 'check output paths up front (asc preflight)',
21
- 'collect finished work and confirm closure (asc controller collect, asc closure)',
22
- 'read incoming requests (asc inbox list/show)',
23
- 'install and check the host (asc host)',
18
+ 'start, pause, resume and finish work inside a contract (asc work)',
19
+ 'see where the work is right now (asc work status)',
20
+ 'check output paths up front (asc work inspect)',
21
+ 'read what is waiting for a person (asc inbox)',
22
+ 'choose who executes an approved act (asc mode)',
23
+ 'see and converge this installation (asc status, asc refresh)',
24
24
  ];
25
25
  const RESOLVE_AGAIN = 'after editing, re-lock with `asc profile resolve --write` (otherwise the next command stops)';
26
26
  export function assessSetup(input) {
@@ -126,11 +126,11 @@ export function renderSetup(status) {
126
126
  const lines = [];
127
127
  switch (status.attachment) {
128
128
  case 'UNATTACHED':
129
- lines.push('Not attached yet — run `asc init --profile <id>` first.');
129
+ lines.push('Not attached yet — run `asc setup` first.');
130
130
  return lines.join('\n');
131
131
  case 'BROKEN':
132
132
  lines.push('Attachment is half-finished: a runtime exists but profile.lock does not.');
133
- lines.push(' Re-attach with `asc init --profile <id>`, or lock it with `asc profile resolve --write`.');
133
+ lines.push(' Re-attach with `asc setup`, or lock it with `asc profile resolve --write`.');
134
134
  return lines.join('\n');
135
135
  case 'LOCK_DRIFT':
136
136
  // 이 상태에서 gate를 나열하면 "설정이 덜 찼다"로 잘못 읽힌다. 원인을 먼저 말한다.
@@ -1,9 +1,9 @@
1
1
  export declare const RUNTIME_PACKAGE = "@asc-agent/runtime";
2
2
  export declare const BOOTSTRAP_PACKAGE = "@asc-agent/bootstrap";
3
3
  /** runtime과 bootstrap은 초기 release에서 lockstep이다. */
4
- export declare const RELEASE_VERSION = "0.7.1";
5
- export declare const RUNTIME_SPEC = "@asc-agent/runtime@0.7.1";
6
- export declare const BOOTSTRAP_SPEC = "@asc-agent/bootstrap@0.7.1";
4
+ export declare const RELEASE_VERSION = "0.8.0";
5
+ export declare const RUNTIME_SPEC = "@asc-agent/runtime@0.8.0";
6
+ export declare const BOOTSTRAP_SPEC = "@asc-agent/bootstrap@0.8.0";
7
7
  /**
8
8
  * 아직 설치되지 않은 machine에서 그대로 실행되는 형태 (C-14 §3.4).
9
9
  *
@@ -9,7 +9,7 @@
9
9
  export const RUNTIME_PACKAGE = '@asc-agent/runtime';
10
10
  export const BOOTSTRAP_PACKAGE = '@asc-agent/bootstrap';
11
11
  /** runtime과 bootstrap은 초기 release에서 lockstep이다. */
12
- export const RELEASE_VERSION = '0.7.1';
12
+ export const RELEASE_VERSION = '0.8.0';
13
13
  export const RUNTIME_SPEC = `${RUNTIME_PACKAGE}@${RELEASE_VERSION}`;
14
14
  export const BOOTSTRAP_SPEC = `${BOOTSTRAP_PACKAGE}@${RELEASE_VERSION}`;
15
15
  /**
@@ -83,7 +83,7 @@ export async function verifyStableInstall(run, expectedVersion = RELEASE_VERSION
83
83
  return {
84
84
  ok: false,
85
85
  state,
86
- remedy: 'Runtime package was installed, but `asc` is not visible in this process. Open a new terminal and run `asc setup status`.',
86
+ remedy: 'Runtime package was installed, but `asc` is not visible in this process. Open a new terminal and run `asc status`.',
87
87
  };
88
88
  }
89
89
  return { ok: false, state, remedy: state.detail };
@@ -32,6 +32,43 @@ export type ExecuteOutcome = {
32
32
  ok: false;
33
33
  reason: 'DRIFT';
34
34
  detail: string;
35
+ }
36
+ /**
37
+ * 실행 직전 재확인이 "지금은 성립하지 않는다" 로 답했다. 밖은 그대로다.
38
+ *
39
+ * 여기서 사람에게 다시 묻지 않는다 — 무엇을 할지는 이미 결정됐고, 이 자리가 답하는
40
+ * 것은 그 결정이 딛고 선 사실이 아직 그대로인가 하나다.
41
+ */
42
+ | {
43
+ ok: false;
44
+ reason: 'NOT_EXECUTABLE';
45
+ detail: string;
46
+ }
47
+ /** 밖에서 거절했다. 나간 것이 없다는 것이 확인된 실패다. */
48
+ | {
49
+ ok: false;
50
+ reason: 'REJECTED';
51
+ detail: string;
52
+ }
53
+ /**
54
+ * 나갔는지 모른다 (0.8.0 §P). 다시 부르지 않는다 — 다시 부르면 같은 것이 두 번 나갈 수
55
+ * 있고, 그것이 이 상태에서 가장 나쁜 결과다. Grant 는 집힌 채로 남아 재사용되지 않는다.
56
+ */
57
+ | {
58
+ ok: false;
59
+ reason: 'UNCERTAIN';
60
+ detail: string;
61
+ }
62
+ /**
63
+ * 나갔는데 되돌려 읽은 것이 기대와 다르다. 성공이라고 적지 않는다 — Grant 는 소진되되
64
+ * `EXECUTED` 가 되지 않는다. 그 상태는 밖에서 "성공적으로 끝남" 으로 읽히기 때문이다.
65
+ */
66
+ | {
67
+ ok: false;
68
+ reason: 'NOT_VERIFIED';
69
+ detail: string;
70
+ resultRef: string;
71
+ mismatches: string[];
35
72
  } | {
36
73
  ok: false;
37
74
  reason: 'ACTION_FAILED';
@@ -42,6 +79,13 @@ export type ExecutorDeps = {
42
79
  scm: ScmPort;
43
80
  /** 이 Physical Run의 식별자. 누가 집었는지 Grant에 남는다. */
44
81
  runId: string;
82
+ /**
83
+ * 되돌려 읽을 수 없는 행위를 실행하지 않는다 (0.8.0 보정 P1-2).
84
+ *
85
+ * 강제가 서 있는 자리(AUTO·실행 축을 읽지 못한 자리)에서 참이다. 사람이 실행하는
86
+ * 자리에서는 그 판단이 사람의 것이므로 호출자가 정한다.
87
+ */
88
+ requireVerification?: boolean;
45
89
  now?: () => string;
46
90
  };
47
91
  export declare class Executor {
@@ -2,22 +2,31 @@
2
2
  //
3
3
  // 이 파일이 시스템에서 유일하게 외부 write를 부르는 지점이다. 순서가 곧 안전장치다:
4
4
  //
5
- // CLAIM (원자적) 계약 범위 확인 Drift Guard 외부 행위 1회 → EXECUTED
5
+ // CLAIM (원자적) 계약 범위 확인 Drift Guard 실행 직전 재검수(읽기)
6
+ // → 외부 행위 1회 → 되돌려 읽기 → EXECUTED
6
7
  //
7
8
  // CLAIM을 먼저 하는 이유는 두 Executor가 같은 Grant로 같은 댓글을 두 번 달지 않게 하기
8
9
  // 위해서고, Drift Guard를 그 다음에 두는 이유는 승인 이후 스레드가 움직였을 때 오래된
9
10
  // 초안이 나가지 않게 하기 위해서다 (OM §11.9). 둘 다 실패는 조용히 넘어가지 않는다.
11
+ //
12
+ // 0.8.0 에서 앞뒤로 한 마디씩 붙었다. 앞의 재검수는 승인이 딛고 선 사실이 아직 그대로인지
13
+ // **읽기만으로** 확인하고(§D), 뒤의 되돌려 읽기는 명령이 0 으로 끝났다는 것과 밖에 그것이
14
+ // 있다는 것이 다르다는 사실을 다룬다(§L·§M·§N). 둘 다 판정은 Core 가 하고, 사실은 Port 가
15
+ // 읽어 온다 — 그래야 사람이 보는 검수와 Agent 가 따르는 검수가 같은 판정이다.
10
16
  import { transitionGrant, transitionRequest } from "../model/transitions.js";
17
+ import { expectationOf, revalidate, verifyAgainst } from "./remote-review.js";
11
18
  import { applyTransition } from "../runtime/store-ops.js";
12
19
  export class Executor {
13
20
  #store;
14
21
  #scm;
15
22
  #runId;
23
+ #requireVerification;
16
24
  #now;
17
25
  constructor(deps) {
18
26
  this.#store = deps.store;
19
27
  this.#scm = deps.scm;
20
28
  this.#runId = deps.runId;
29
+ this.#requireVerification = deps.requireVerification ?? false;
21
30
  this.#now = deps.now ?? (() => new Date().toISOString());
22
31
  }
23
32
  async run(grantId) {
@@ -42,33 +51,102 @@ export class Executor {
42
51
  // 계약 자체가 모순이면 외부 상태를 조회할 이유도 없으므로 Drift Guard보다 앞에 둔다.
43
52
  if (!claimed.entity.allowedWrites.includes(claimed.entity.action)) {
44
53
  const detail = `'${claimed.entity.action}' is not in allowed writes [${claimed.entity.allowedWrites.join(', ')}]`;
45
- await this.#close(grant.id, 'INVALIDATED', this.#now(), detail);
54
+ await this.#close(grant.id, 'INVALIDATED', this.#now(), detail, 'FORBIDDEN');
46
55
  return { ok: false, reason: 'FORBIDDEN_ACTION', detail };
47
56
  }
48
57
  // 3. Drift Guard — 승인 시점의 기준선과 지금을 대조한다
49
58
  const drift = await this.#detectDrift(claimed.entity);
50
59
  if (drift) {
51
- await this.#close(grant.id, 'INVALIDATED', this.#now(), drift);
60
+ await this.#close(grant.id, 'INVALIDATED', this.#now(), drift, 'DRIFT');
52
61
  return { ok: false, reason: 'DRIFT', detail: drift };
53
62
  }
54
- // 4. 외부 행위 1회. payload는 승인된 내용 그대로 나간다
55
- const result = await this.#scm.execute({
63
+ const action = {
56
64
  action: claimed.entity.action,
57
65
  target: claimed.entity.target,
58
66
  payload: claimed.entity.payload,
59
- });
67
+ };
68
+ // 4. 실행 직전 재확인. 승인은 그때의 사실 위에서 났다 — **바뀔 수 있는 것만**
69
+ // 읽기로 다시 본다. 모호함·읽지 못한 사실은 결정 시점에 이미 사람이 넘어간 것이고,
70
+ // 여기서 다시 꺼내면 그것이 두 번째 승인 벽이 된다.
71
+ let expected;
72
+ if (this.#scm.review) {
73
+ const facts = await this.#scm.review(action);
74
+ // 되돌려 읽을 수 없는 행위는 자율 실행에서 내보내지 않는다 — 나간 뒤에 아무도
75
+ // 확인할 수 없는 쓰기가 된다. 사람이 실행하는 자리에서는 그 판단이 사람의 것이다.
76
+ if (this.#requireVerification && facts.verifiable === false) {
77
+ const detail = `${facts.provider} cannot read back '${action.action}'`;
78
+ await this.#close(grant.id, 'INVALIDATED', this.#now(), detail, 'NOT_EXECUTABLE');
79
+ return { ok: false, reason: 'NOT_EXECUTABLE', detail };
80
+ }
81
+ const moved = revalidate({
82
+ action: action.action,
83
+ facts,
84
+ ...(claimed.entity.basis ? { basis: claimed.entity.basis } : {}),
85
+ });
86
+ if (moved) {
87
+ await this.#close(grant.id, 'INVALIDATED', this.#now(), `${moved.code}: ${moved.detail}`, moved.code === 'DRIFT' ? 'DRIFT' : 'NOT_EXECUTABLE');
88
+ return { ok: false, reason: moved.code === 'DRIFT' ? 'DRIFT' : 'NOT_EXECUTABLE', detail: moved.detail };
89
+ }
90
+ expected = expectationOf({ action: action.action, target: action.target, facts, ...(claimed.entity.basis ? { basis: claimed.entity.basis } : {}) });
91
+ }
92
+ // 5. 외부 행위 1회. payload는 승인된 내용 그대로 나간다
93
+ const result = await this.#scm.execute(action);
60
94
  if (!result.ok) {
61
- // 재시도하지 않는다. 실패한 호출이 정말 나가지 않았는지는 여기서 없고,
62
- // 모른 채 다시 부르면 같은 글이 두 번 올라간다. 사람이 확인하고 새 Grant를 낸다.
63
- await this.#close(grant.id, 'INVALIDATED', this.#now(), `실행 실패: ${result.error}`);
64
- return { ok: false, reason: 'ACTION_FAILED', detail: result.error };
95
+ // **나갔는지 모르는 실패와 거절을 가른다** (0.8.0 §P). 어느 쪽이든 다시 부르지
96
+ // 않는다 — 모른 채 재시도하면 같은 것이 두 번 나갈 있다.
97
+ if (uncertain(result.error)) {
98
+ await this.#store.appendHistory({
99
+ at: this.#now(),
100
+ actor: this.#runId,
101
+ kind: 'external_action_uncertain',
102
+ ref: grant.id,
103
+ detail: `${grant.action} → ${grant.target}: ${result.error}`,
104
+ });
105
+ // **CLAIMED 로 남기지 않는다** (0.8.0 보정 P1-1). 그 상태는 이 시스템에서 "지금
106
+ // 누가 집고 실행 중" 을 뜻하고(READY→CLAIMED→EXECUTED/INVALIDATED), 아무도 실행
107
+ // 중이 아닌 Grant 를 거기 두면 화면과 감사가 영영 틀린 말을 한다. terminal 로
108
+ // 닫되 **이유가 UNCERTAIN** 이다 — 재실행은 막히고, 성공도 실패도 주장하지 않는다.
109
+ await this.#close(grant.id, 'INVALIDATED', this.#now(), `결과 불명: ${result.error}`, 'UNCERTAIN');
110
+ return { ok: false, reason: 'UNCERTAIN', detail: result.error };
111
+ }
112
+ await this.#close(grant.id, 'INVALIDATED', this.#now(), `실행 실패: ${result.error}`, 'REJECTED');
113
+ return { ok: false, reason: 'REJECTED', detail: result.error };
114
+ }
115
+ // 6. 되돌려 읽기. exit 0 은 성공이 아니다.
116
+ let mismatches = [];
117
+ if (this.#scm.verify && expected) {
118
+ const read = await this.#scm.verify(action, { resultRef: result.resultRef });
119
+ if (!read.unsupported) {
120
+ const comparable = Object.keys(expected).filter((key) => key !== 'action' && key !== 'target' && read.observed[key] !== undefined);
121
+ const verified = verifyAgainst(expected, read.observed, comparable);
122
+ mismatches = comparable.length === 0 ? ['nothing could be read back to compare'] : verified.mismatches;
123
+ }
65
124
  }
66
- // 5. 소비 기록 성공한 Grant는 다시 없다
125
+ // 7. 소비 기록. **되돌려 읽은 것이 다르면 EXECUTED 적지 않는다** (P0-5) —
126
+ // 그 상태는 밖에서 "성공적으로 끝남" 으로 읽힌다. 나간 것은 나갔으므로 재사용은
127
+ // 막되(terminal), 성공은 주장하지 않는다.
67
128
  const executedAt = this.#now();
129
+ if (mismatches.length > 0) {
130
+ await this.#close(grant.id, 'INVALIDATED', executedAt, `되돌려 읽은 것이 다르다: ${mismatches.join('; ')}`, 'NOT_VERIFIED', result.resultRef);
131
+ await this.#store.appendHistory({
132
+ at: executedAt,
133
+ actor: this.#runId,
134
+ kind: 'external_action_unverified',
135
+ ref: grant.id,
136
+ detail: `${grant.action} → ${grant.target} = ${result.resultRef} (unverified: ${mismatches.join('; ')})`,
137
+ });
138
+ return {
139
+ ok: false,
140
+ reason: 'NOT_VERIFIED',
141
+ detail: mismatches.join('; '),
142
+ resultRef: result.resultRef,
143
+ mismatches,
144
+ };
145
+ }
68
146
  const executed = await applyTransition(this.#store, 'grant', grant.id, (g) => transitionGrant(g, 'EXECUTED', 'executor', { resultRef: result.resultRef, consumedAt: executedAt }));
69
147
  if (!executed.ok)
70
148
  return { ok: false, reason: 'CLAIMED_BY_OTHER' };
71
- // 6. 요청이 근거였다면 그 요청을 닫는다 — 외부에 무엇이 남았는지 요청에서 바로
149
+ // 8. 요청이 근거였다면 그 요청을 닫는다 — 외부에 무엇이 남았는지 요청에서 바로
72
150
  // 따라갈 수 있어야 한다. 세션이 근거인 경우에는 닫을 요청이 없고, 그 자취는
73
151
  // 아래 History 와 세션 자신의 기록에 남는다.
74
152
  if (claimed.entity.requestId) {
@@ -103,8 +181,24 @@ export class Executor {
103
181
  }
104
182
  return null;
105
183
  }
106
- async #close(grantId, to, at, detail) {
107
- await applyTransition(this.#store, 'grant', grantId, (g) => transitionGrant(g, to, 'executor'));
184
+ /**
185
+ * terminal 닫는다. **이유를 함께 적는다** 상태 다섯 개만으로는 "왜" 가 남지 않고,
186
+ * 소진된 것과 성공한 것을 가르는 것도 그 이유다 (0.8.0 보정 P0-5).
187
+ */
188
+ async #close(grantId, to, at, detail, resolution, resultRef) {
189
+ await applyTransition(this.#store, 'grant', grantId, (g) => transitionGrant(g, to, 'executor', {
190
+ ...(resolution ? { resolution } : {}),
191
+ ...(resultRef ? { resultRef } : {}),
192
+ }));
108
193
  await this.#store.appendHistory({ at, actor: this.#runId, kind: `grant_${to.toLowerCase()}`, ref: grantId, detail });
109
194
  }
110
195
  }
196
+ /**
197
+ * 이 실패가 "나가지 않았다" 인가, "모른다" 인가 (0.8.0 §P).
198
+ *
199
+ * 시간이 끊기거나 연결이 죽은 자리에서는 요청이 도착했는지 알 수 없다. 그 상태를 실패로
200
+ * 적고 재시도하면 같은 것이 두 번 나갈 수 있으므로, 모르는 것은 모른다고 적는다.
201
+ */
202
+ function uncertain(error) {
203
+ return /timeout|timed out|ETIMEDOUT|ECONNRESET|EAI_AGAIN|socket hang up|aborted|network/i.test(error);
204
+ }
@@ -15,6 +15,8 @@ export type IssueGrantInput = {
15
15
  /** 이 계약으로 허용되는 행위 목록. 비우면 `action` 하나만 허용된다. */
16
16
  allowedWrites?: string[];
17
17
  issuedAt: string;
18
+ /** 승인이 딛고 선 사실 (0.8.0 §L). 실행 직전 재검수가 이 값과 지금을 견준다. */
19
+ basis?: ExecutionGrant['basis'];
18
20
  };
19
21
  /**
20
22
  * 세션이 만든 결과를 내보내는 경우의 입력 (0.7.0).
@@ -43,6 +45,12 @@ export type IssueForSessionInput = {
43
45
  issuedAt: string;
44
46
  /** 게시 직전 대조할 기준 (OM §11.9). 없으면 대조하지 않는다. */
45
47
  snapshot?: CanonicalSnapshot[];
48
+ /**
49
+ * 승인이 딛고 선 사실 (0.8.0 §L). 검수가 읽어 온 값을 그대로 못 박는다 — 실행 직전
50
+ * 재검수가 이 값과 지금을 견주므로, 승인은 "이 가지" 가 아니라 "이 commit" 에 대한
51
+ * 것이 된다.
52
+ */
53
+ basis?: ExecutionGrant['basis'];
46
54
  };
47
55
  export type IssueFailure = {
48
56
  kind: 'REQUEST_NOT_FOUND';
@@ -58,6 +58,7 @@ export class GrantService {
58
58
  payload,
59
59
  // 이 시점의 정본이 Drift Guard의 기준선이 된다 (OM §11.9)
60
60
  snapshot: request.snapshot,
61
+ ...(input.basis ? { basis: input.basis } : {}),
61
62
  ...(request.source.threadLastEventId !== undefined
62
63
  ? { threadLastEventId: request.source.threadLastEventId }
63
64
  : {}),
@@ -121,6 +122,7 @@ export class GrantService {
121
122
  payload: input.payload,
122
123
  snapshot: input.snapshot ?? [],
123
124
  allowedWrites: input.allowedWrites ?? [input.action],
125
+ ...(input.basis ? { basis: input.basis } : {}),
124
126
  });
125
127
  const created = await this.#store.create('grant', grant);
126
128
  if (!created.ok)
@@ -0,0 +1,117 @@
1
+ /** 검수가 본 것. provider adapter 가 읽어서 넘긴다 (mutation 0). */
2
+ export type RemoteFacts = {
3
+ provider: string;
4
+ /** 정규화된 원격 신원 — 이 결합이 가리키는 프로젝트/저장소와 견줄 값이다. */
5
+ resource?: string;
6
+ target?: string;
7
+ /** 이 통로가 이 행위를 할 수 있는가. */
8
+ capability?: boolean;
9
+ /** 이 통로가 이 행위를 되돌려 읽을 수 있는가 (0.8.0 보정 P1-2). */
10
+ verifiable?: boolean;
11
+ /**
12
+ * 관측된 사실들. 키는 provider 가 정하고 Core 는 해석하지 않는다 — 화면과 감사에
13
+ * 그대로 남는다. 예: `local.head`, `remote.sha`, `mr.state`, `remote.url`.
14
+ */
15
+ observed?: Readonly<Record<string, string | undefined>>;
16
+ /** 고를 수 없는 상태. 같은 대상이 둘 이상 후보로 보일 때 그 이유를 적는다. */
17
+ ambiguity?: readonly string[];
18
+ /** 읽지 못한 것. 모르는 것을 안다고 적지 않는다. */
19
+ unknown?: readonly string[];
20
+ };
21
+ /** 승인 시점에 못 박힌 것. 실행 직전 재검수는 이 값과 지금을 견준다. */
22
+ export type ApprovedBasis = {
23
+ /** 올리기로 한 그 commit. 가지 이름만으로는 같은 승인이 다른 내용을 내보낸다. */
24
+ sourceSha?: string;
25
+ /** 승인 당시의 원격 상태. 그 뒤 남이 움직였는지 보는 기준선이다. */
26
+ remoteBaseline?: string;
27
+ /** 이 결합이 가리키는 신원. target 이 여기서 벗어나면 관리 범위 밖이다. */
28
+ resource?: string;
29
+ };
30
+ export type ReviewSeverity = 'BLOCK' | 'REVIEW' | 'NOTE';
31
+ export type ReviewFinding = {
32
+ code: 'NO_CAPABILITY' | 'NO_VERIFY_PATH' | 'BINDING_MISMATCH' | 'DRIFT' | 'AMBIGUOUS' | 'UNKNOWN_FACT' | 'MISSING_TARGET' | 'NOTE';
33
+ severity: ReviewSeverity;
34
+ detail: string;
35
+ };
36
+ /**
37
+ * READY 사실이 맞는다. 결정권이 이미 풀렸다면 그대로 나갈 수 있다
38
+ * REVIEW_REQUIRED 사람이 봐야 하는 것이 있다 — 범위 밖 대상, 모호함, 읽지 못한 사실
39
+ * NOT_EXECUTABLE 지금 이 행동은 성립하지 않는다 — 할 수 없거나, 근거가 이미 움직였다
40
+ */
41
+ export type ReviewVerdict = 'READY' | 'REVIEW_REQUIRED' | 'NOT_EXECUTABLE';
42
+ export type ReviewOutcome = {
43
+ verdict: ReviewVerdict;
44
+ findings: ReviewFinding[];
45
+ /** 실행이 성공했다면 밖에서 무엇이 보여야 하는가. verify 가 이 값과 견준다. */
46
+ expected: Record<string, string>;
47
+ };
48
+ export type ReviewInput = {
49
+ action: string;
50
+ target: string;
51
+ facts: RemoteFacts;
52
+ basis?: ApprovedBasis;
53
+ /**
54
+ * 되돌려 읽을 수 없으면 실행하지 않는가 (0.8.0 보정 P1-2).
55
+ *
56
+ * 자율 실행(AUTO)에서는 참이다: 확인할 수 없는 쓰기를 자율로 내보내면 "성공했다" 를
57
+ * 아무도 세지 않는다. 사람이 실행하는 자리(MANUAL)에서는 그 판단이 사람의 것이다.
58
+ */
59
+ requireVerification?: boolean;
60
+ };
61
+ /**
62
+ * 사실을 판정한다. **모르는 것을 무조건 차단으로 번역하지 않는다** (§E) — 모르는 것은
63
+ * 사람이 볼 일이고, 성립하지 않는 것만 실행 불가다.
64
+ */
65
+ export declare function reviewExternalAction(input: ReviewInput): ReviewOutcome;
66
+ /**
67
+ * 성공했다면 밖에서 무엇이 보여야 하는가.
68
+ *
69
+ * 실행 전에 적어 두는 이유는 하나다: 실행 뒤에 기대치를 정하면 관측한 것이 기대치가 된다.
70
+ */
71
+ export declare function expectationOf(input: {
72
+ action: string;
73
+ target: string;
74
+ facts: RemoteFacts;
75
+ basis?: ApprovedBasis;
76
+ }): Record<string, string>;
77
+ /**
78
+ * 실행 직전의 재확인 — **바뀔 수 있는 것만 본다.**
79
+ *
80
+ * 검수(CHECK)는 계약을 만들 때 이미 끝났다. 그때 사람이 보고 넘어간 모호함·읽지 못한
81
+ * 사실을 실행 직전에 다시 꺼내면 그것은 두 번째 승인 벽이 된다 — 같은 질문에 두 번
82
+ * 답하게 만드는 구조이고, 이 릴리스가 없애려는 바로 그 형태다.
83
+ *
84
+ * 그래서 여기서 묻는 것은 **승인 이후 실제로 움직일 수 있는 것** 셋뿐이다:
85
+ *
86
+ * ```text
87
+ * 이 통로가 아직 이 행위를 할 수 있는가
88
+ * 대상이 아직 승인된 범위 안인가
89
+ * 못 박은 commit·기준선이 아직 그대로인가
90
+ * ```
91
+ *
92
+ * 답은 둘이다: 그대로면 `null`, 아니면 실행하지 않을 이유 하나.
93
+ */
94
+ export declare function revalidate(input: {
95
+ action: string;
96
+ facts: RemoteFacts;
97
+ basis?: ApprovedBasis;
98
+ }): {
99
+ code: 'NO_CAPABILITY' | 'BINDING_MISMATCH' | 'DRIFT';
100
+ detail: string;
101
+ } | null;
102
+ /** 사람이 읽는 한 줄들. 화면 형태는 MANUAL·AUTO 가 각자 정하고 판정은 같은 것을 쓴다. */
103
+ export declare function reviewLines(outcome: ReviewOutcome): string[];
104
+ /** 원격 신원 비교용 정규화. 대소문자·`.git`·앞뒤 슬래시만 걷어낸다. */
105
+ export declare function normalize(resource: string): string;
106
+ /**
107
+ * 실행 뒤의 대조. **명령이 0 으로 끝났다는 것은 성공이 아니다** (§L·§M·§N).
108
+ *
109
+ * 되돌려 읽은 사실이 기대치와 다르면 성공이라고 적지 않는다.
110
+ */
111
+ export type VerifyOutcome = {
112
+ ok: boolean;
113
+ /** 무엇을 봤는가. 감사에 그대로 남는다. */
114
+ observed: Record<string, string | undefined>;
115
+ mismatches: string[];
116
+ };
117
+ export declare function verifyAgainst(expected: Readonly<Record<string, string>>, observed: Readonly<Record<string, string | undefined>>, keys: readonly string[]): VerifyOutcome;