@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,188 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ScopedStore } from '../../ports/state-store.ts';
|
|
3
|
+
/**
|
|
4
|
+
* 올릴 자격이 되는 사유 (C-13 §1.1). **이 목록 밖으로는 올리지 않는다.**
|
|
5
|
+
*
|
|
6
|
+
* 불확실성(uncertain·multiple_options·want_confirmation 계열)은 여기 없다. 그것은
|
|
7
|
+
* 경계가 아니라 상태이며, 올려 봐야 사람은 Agent보다 적은 근거로 결정하게 된다.
|
|
8
|
+
*/
|
|
9
|
+
export declare const EscalationPredicate: z.ZodEnum<["ownership_boundary", "shared_contract_change", "acceptance_change", "secret_or_permission", "irreversible_action", "explicit_rule_requires_approval", "canonical_conflict"]>;
|
|
10
|
+
export type EscalationPredicate = z.infer<typeof EscalationPredicate>;
|
|
11
|
+
export declare const ESCALATION_ID: RegExp;
|
|
12
|
+
export declare const EscalationRecord: z.ZodObject<{
|
|
13
|
+
escalationId: z.ZodString;
|
|
14
|
+
sessionId: z.ZodString;
|
|
15
|
+
/** 올린 주체. 누가 올렸는지 모르는 상신은 감사 대상이 아니다. */
|
|
16
|
+
openedBy: z.ZodString;
|
|
17
|
+
predicates: z.ZodArray<z.ZodEnum<["ownership_boundary", "shared_contract_change", "acceptance_change", "secret_or_permission", "irreversible_action", "explicit_rule_requires_approval", "canonical_conflict"]>, "atleastone">;
|
|
18
|
+
/** 사람이 답할 한 문장. */
|
|
19
|
+
question: z.ZodString;
|
|
20
|
+
/** 근거 없는 상신은 상신이 아니다. */
|
|
21
|
+
evidenceRefs: z.ZodArray<z.ZodString, "atleastone">;
|
|
22
|
+
affectedNodes: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
23
|
+
/**
|
|
24
|
+
* 이것 때문에 **지금 못 하는 작업 노드** (Done Criteria 항목).
|
|
25
|
+
* 경계 영역(blockedScope)과 다른 축이다 (C-13 §2.1).
|
|
26
|
+
*/
|
|
27
|
+
blockedNodes: z.ZodArray<z.ZodString, "atleastone">;
|
|
28
|
+
/** 경계의 실체 — 경로·영역 범위. 노드 목록이 아니다. */
|
|
29
|
+
blockedScope: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
30
|
+
/** 계속 갈 수 있는 노드. **자동 계산이며 올리는 쪽이 적지 않는다** (불변식 ③). */
|
|
31
|
+
stillRunnableNodes: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
32
|
+
boundaryFingerprint: z.ZodString;
|
|
33
|
+
previousEscalationId: z.ZodOptional<z.ZodString>;
|
|
34
|
+
whyPreviousDecisionDoesNotCoverThis: z.ZodOptional<z.ZodString>;
|
|
35
|
+
/** 이 상신으로 만들어진 ApprovalRequest. 결정은 기존 inbox 경로가 한다. */
|
|
36
|
+
requestId: z.ZodString;
|
|
37
|
+
openedAt: z.ZodString;
|
|
38
|
+
}, "strip", z.ZodTypeAny, {
|
|
39
|
+
evidenceRefs: [string, ...string[]];
|
|
40
|
+
escalationId: string;
|
|
41
|
+
predicates: ["ownership_boundary" | "shared_contract_change" | "acceptance_change" | "secret_or_permission" | "irreversible_action" | "explicit_rule_requires_approval" | "canonical_conflict", ...("ownership_boundary" | "shared_contract_change" | "acceptance_change" | "secret_or_permission" | "irreversible_action" | "explicit_rule_requires_approval" | "canonical_conflict")[]];
|
|
42
|
+
affectedNodes: string[];
|
|
43
|
+
blockedNodes: [string, ...string[]];
|
|
44
|
+
blockedScope: string[];
|
|
45
|
+
stillRunnableNodes: string[];
|
|
46
|
+
requestId: string;
|
|
47
|
+
sessionId: string;
|
|
48
|
+
openedBy: string;
|
|
49
|
+
question: string;
|
|
50
|
+
boundaryFingerprint: string;
|
|
51
|
+
openedAt: string;
|
|
52
|
+
previousEscalationId?: string | undefined;
|
|
53
|
+
whyPreviousDecisionDoesNotCoverThis?: string | undefined;
|
|
54
|
+
}, {
|
|
55
|
+
evidenceRefs: [string, ...string[]];
|
|
56
|
+
escalationId: string;
|
|
57
|
+
predicates: ["ownership_boundary" | "shared_contract_change" | "acceptance_change" | "secret_or_permission" | "irreversible_action" | "explicit_rule_requires_approval" | "canonical_conflict", ...("ownership_boundary" | "shared_contract_change" | "acceptance_change" | "secret_or_permission" | "irreversible_action" | "explicit_rule_requires_approval" | "canonical_conflict")[]];
|
|
58
|
+
blockedNodes: [string, ...string[]];
|
|
59
|
+
requestId: string;
|
|
60
|
+
sessionId: string;
|
|
61
|
+
openedBy: string;
|
|
62
|
+
question: string;
|
|
63
|
+
boundaryFingerprint: string;
|
|
64
|
+
openedAt: string;
|
|
65
|
+
affectedNodes?: string[] | undefined;
|
|
66
|
+
blockedScope?: string[] | undefined;
|
|
67
|
+
stillRunnableNodes?: string[] | undefined;
|
|
68
|
+
previousEscalationId?: string | undefined;
|
|
69
|
+
whyPreviousDecisionDoesNotCoverThis?: string | undefined;
|
|
70
|
+
}>;
|
|
71
|
+
export type EscalationRecord = z.infer<typeof EscalationRecord>;
|
|
72
|
+
/** 막힌 상신. 무엇을 올리려 했는지가 남아야 Gate를 검증할 수 있다. */
|
|
73
|
+
export declare const RejectedEscalation: z.ZodObject<{
|
|
74
|
+
sessionId: z.ZodString;
|
|
75
|
+
openedBy: z.ZodString;
|
|
76
|
+
question: z.ZodString;
|
|
77
|
+
/** 올린 쪽이 사유라고 적은 것. predicate가 아니어서 막혔다. */
|
|
78
|
+
claimedReasons: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
79
|
+
reason: z.ZodEnum<["APPROVAL_NOT_JUSTIFIED", "DUPLICATE_EPISODE", "RESUBMIT_NOT_JUSTIFIED"]>;
|
|
80
|
+
detail: z.ZodString;
|
|
81
|
+
at: z.ZodString;
|
|
82
|
+
}, "strip", z.ZodTypeAny, {
|
|
83
|
+
at: string;
|
|
84
|
+
sessionId: string;
|
|
85
|
+
detail: string;
|
|
86
|
+
reason: "APPROVAL_NOT_JUSTIFIED" | "DUPLICATE_EPISODE" | "RESUBMIT_NOT_JUSTIFIED";
|
|
87
|
+
openedBy: string;
|
|
88
|
+
question: string;
|
|
89
|
+
claimedReasons: string[];
|
|
90
|
+
}, {
|
|
91
|
+
at: string;
|
|
92
|
+
sessionId: string;
|
|
93
|
+
detail: string;
|
|
94
|
+
reason: "APPROVAL_NOT_JUSTIFIED" | "DUPLICATE_EPISODE" | "RESUBMIT_NOT_JUSTIFIED";
|
|
95
|
+
openedBy: string;
|
|
96
|
+
question: string;
|
|
97
|
+
claimedReasons?: string[] | undefined;
|
|
98
|
+
}>;
|
|
99
|
+
export type RejectedEscalation = z.infer<typeof RejectedEscalation>;
|
|
100
|
+
export type OpenInput = {
|
|
101
|
+
escalationId: string;
|
|
102
|
+
sessionId: string;
|
|
103
|
+
openedBy: string;
|
|
104
|
+
/** 자유 문자열로 받는다 — 유효하지 않은 사유도 **무엇을 적었는지 남기기 위해** 받는다. */
|
|
105
|
+
predicates: readonly string[];
|
|
106
|
+
question: string;
|
|
107
|
+
evidenceRefs?: readonly string[];
|
|
108
|
+
affectedNodes?: readonly string[];
|
|
109
|
+
blockedNodes: readonly string[];
|
|
110
|
+
blockedScope?: readonly string[];
|
|
111
|
+
/** 이 세션의 완료 조건 전부. stillRunnableNodes를 여기서 뺀다. */
|
|
112
|
+
doneCriteria?: readonly string[];
|
|
113
|
+
previousEscalationId?: string;
|
|
114
|
+
whyPreviousDecisionDoesNotCoverThis?: string;
|
|
115
|
+
openedAt?: string;
|
|
116
|
+
};
|
|
117
|
+
export type OpenOutcome = {
|
|
118
|
+
ok: true;
|
|
119
|
+
record: EscalationRecord;
|
|
120
|
+
} | {
|
|
121
|
+
ok: false;
|
|
122
|
+
reason: 'APPROVAL_NOT_JUSTIFIED' | 'DUPLICATE_EPISODE' | 'RESUBMIT_NOT_JUSTIFIED' | 'INVALID_INPUT';
|
|
123
|
+
detail: string;
|
|
124
|
+
/** 이미 열려 있는 같은 경계. 사람이 그것을 보면 된다. */
|
|
125
|
+
existing?: EscalationRecord;
|
|
126
|
+
};
|
|
127
|
+
export type ResolveOutcome = {
|
|
128
|
+
ok: true;
|
|
129
|
+
record: EscalationRecord;
|
|
130
|
+
} | {
|
|
131
|
+
ok: false;
|
|
132
|
+
reason: 'NOT_FOUND' | 'ALREADY_RESOLVED';
|
|
133
|
+
detail: string;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* 같은 경계인가 (C-13 §5).
|
|
137
|
+
*
|
|
138
|
+
* **evidence는 넣지 않는다** (불변식 ⑦). 넣으면 근거 한 줄만 더 붙여 같은 질문을 다시
|
|
139
|
+
* 올릴 수 있고, 그게 정확히 Approval Budget이 막으려는 것이다.
|
|
140
|
+
*/
|
|
141
|
+
export declare function boundaryFingerprint(input: {
|
|
142
|
+
predicates: readonly string[];
|
|
143
|
+
blockedNodes: readonly string[];
|
|
144
|
+
blockedScope?: readonly string[];
|
|
145
|
+
}): string;
|
|
146
|
+
export declare class EscalationLedger {
|
|
147
|
+
#private;
|
|
148
|
+
constructor(scope: ScopedStore, now?: () => string);
|
|
149
|
+
/**
|
|
150
|
+
* 상신을 연다 — **이 함수가 Gate다.**
|
|
151
|
+
*
|
|
152
|
+
* 통과하면 record를 남기고 호출자가 ApprovalRequest를 만든다. 막히면 request는
|
|
153
|
+
* 만들어지지 않고 막힌 사실이 남는다.
|
|
154
|
+
*/
|
|
155
|
+
open(input: OpenInput): Promise<OpenOutcome>;
|
|
156
|
+
/** 만들어진 ApprovalRequest를 잇는다. record는 이미 확정됐고 참조만 채운다. */
|
|
157
|
+
attachRequest(escalationId: string, requestId: string): Promise<boolean>;
|
|
158
|
+
/**
|
|
159
|
+
* 사람이 결정해 닫혔다. **여기서 결정을 대신하지 않는다** — 결정은 기존 Approval
|
|
160
|
+
* 경로에서 일어나고, 이 마커는 "그 결정이 이 경계를 덮었다"는 사실만 남긴다.
|
|
161
|
+
*/
|
|
162
|
+
resolve(escalationId: string, decidedBy: string, decisionRef: string, at?: string): Promise<ResolveOutcome>;
|
|
163
|
+
/** 아직 사람이 결정하지 않은 상신. 이것이 곧 "무엇을 기다리는가"다. */
|
|
164
|
+
pending(): Promise<EscalationRecord[]>;
|
|
165
|
+
all(): Promise<EscalationRecord[]>;
|
|
166
|
+
get(escalationId: string): Promise<EscalationRecord | null>;
|
|
167
|
+
/** 막힌 상신들. Gate가 실제로 무엇을 막았는지 사람이 볼 수 있어야 한다. */
|
|
168
|
+
rejected(): Promise<RejectedEscalation[]>;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 지금 무엇이 막혔고 무엇이 갈 수 있는가 (C-13 §6).
|
|
172
|
+
*
|
|
173
|
+
* **입력은 상신 기록뿐이다.** Checkpoint.blockers 같은 서술 문자열을 읽지 않는다
|
|
174
|
+
* (불변식 ⑤) — 문구를 바꾸는 것이 곧 권한 변경이 되면 안 된다.
|
|
175
|
+
*
|
|
176
|
+
* 결과는 `deriveExecutionState`(C-10 §7)가 그대로 소비한다. 새 상태 enum도, 새 그래프도
|
|
177
|
+
* 만들지 않는다: node는 Done Criteria 항목이고 막힌 것만 빠진다.
|
|
178
|
+
*/
|
|
179
|
+
export declare function proceedGateFacts(pending: readonly EscalationRecord[], doneCriteria: readonly string[]): {
|
|
180
|
+
waitingOn: string[];
|
|
181
|
+
conditions: string[];
|
|
182
|
+
runnable: string[];
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* 사람이 읽는 줄. **왜 이것이 Human Boundary인지가 먼저 온다** (C-13 §7).
|
|
186
|
+
* "어떻게 할까요"가 아니라 "이 경계라서 당신 몫이다"가 상신의 형태다.
|
|
187
|
+
*/
|
|
188
|
+
export declare function escalationLines(records: readonly EscalationRecord[]): string[];
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// Escalation — 사람에게 올릴 자격이 있는가 (C-13).
|
|
2
|
+
//
|
|
3
|
+
// 지금까지 ASC에는 승인 통로가 다 있었는데 그 통로에 들어갈 자격 조건이 없었다.
|
|
4
|
+
// 그래서 "확신이 안 서서" 올린 것과 "내 권한 밖이라" 올린 것이 같은 모양으로 도착했고,
|
|
5
|
+
// 사람은 둘 다 읽어야 했다.
|
|
6
|
+
//
|
|
7
|
+
// 이 파일이 하는 일은 하나다: **자격 없는 상신을 ApprovalRequest 앞에서 막는다.**
|
|
8
|
+
//
|
|
9
|
+
// predicate ≥ 1 → 기존 Approval 경로로 보낸다 (C-01 무수정, 결정 표면은 그대로)
|
|
10
|
+
// predicate 0 → 만들지 않는다. 그리고 **막았다는 사실을 남긴다**
|
|
11
|
+
//
|
|
12
|
+
// 마지막 줄이 중요하다. 조용히 거절하면 Gate가 제 일을 하는지 아무도 못 본다 —
|
|
13
|
+
// Bounded Query가 막힌 시도를 violation으로 남기는 것과 같은 이유다 (C-04 §5.2).
|
|
14
|
+
//
|
|
15
|
+
// **판정 입력은 여기 적힌 구조뿐이다.** Checkpoint.blockers 같은 서술 문자열을 읽어
|
|
16
|
+
// 자동 판정하지 않는다 (C-13 불변식 ⑤) — 문구를 바꾸는 것이 곧 권한 변경이 되면 안 된다.
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { SessionId } from "../model/ids.js";
|
|
20
|
+
/**
|
|
21
|
+
* 올릴 자격이 되는 사유 (C-13 §1.1). **이 목록 밖으로는 올리지 않는다.**
|
|
22
|
+
*
|
|
23
|
+
* 불확실성(uncertain·multiple_options·want_confirmation 계열)은 여기 없다. 그것은
|
|
24
|
+
* 경계가 아니라 상태이며, 올려 봐야 사람은 Agent보다 적은 근거로 결정하게 된다.
|
|
25
|
+
*/
|
|
26
|
+
export const EscalationPredicate = z.enum([
|
|
27
|
+
'ownership_boundary',
|
|
28
|
+
'shared_contract_change',
|
|
29
|
+
'acceptance_change',
|
|
30
|
+
'secret_or_permission',
|
|
31
|
+
'irreversible_action',
|
|
32
|
+
'explicit_rule_requires_approval',
|
|
33
|
+
'canonical_conflict',
|
|
34
|
+
]);
|
|
35
|
+
export const ESCALATION_ID = /^ESC-\d{8}-\d{2}$/;
|
|
36
|
+
export const EscalationRecord = z.object({
|
|
37
|
+
escalationId: z.string().regex(ESCALATION_ID),
|
|
38
|
+
sessionId: SessionId,
|
|
39
|
+
/** 올린 주체. 누가 올렸는지 모르는 상신은 감사 대상이 아니다. */
|
|
40
|
+
openedBy: z.string().min(1),
|
|
41
|
+
predicates: z.array(EscalationPredicate).nonempty(),
|
|
42
|
+
/** 사람이 답할 한 문장. */
|
|
43
|
+
question: z.string().min(1),
|
|
44
|
+
/** 근거 없는 상신은 상신이 아니다. */
|
|
45
|
+
evidenceRefs: z.array(z.string().min(1)).nonempty(),
|
|
46
|
+
affectedNodes: z.array(z.string()).default([]),
|
|
47
|
+
/**
|
|
48
|
+
* 이것 때문에 **지금 못 하는 작업 노드** (Done Criteria 항목).
|
|
49
|
+
* 경계 영역(blockedScope)과 다른 축이다 (C-13 §2.1).
|
|
50
|
+
*/
|
|
51
|
+
blockedNodes: z.array(z.string().min(1)).nonempty(),
|
|
52
|
+
/** 경계의 실체 — 경로·영역 범위. 노드 목록이 아니다. */
|
|
53
|
+
blockedScope: z.array(z.string()).default([]),
|
|
54
|
+
/** 계속 갈 수 있는 노드. **자동 계산이며 올리는 쪽이 적지 않는다** (불변식 ③). */
|
|
55
|
+
stillRunnableNodes: z.array(z.string()).default([]),
|
|
56
|
+
boundaryFingerprint: z.string().min(1),
|
|
57
|
+
previousEscalationId: z.string().optional(),
|
|
58
|
+
whyPreviousDecisionDoesNotCoverThis: z.string().optional(),
|
|
59
|
+
/** 이 상신으로 만들어진 ApprovalRequest. 결정은 기존 inbox 경로가 한다. */
|
|
60
|
+
requestId: z.string().min(1),
|
|
61
|
+
openedAt: z.string().min(1),
|
|
62
|
+
});
|
|
63
|
+
/** 막힌 상신. 무엇을 올리려 했는지가 남아야 Gate를 검증할 수 있다. */
|
|
64
|
+
export const RejectedEscalation = z.object({
|
|
65
|
+
sessionId: z.string().min(1),
|
|
66
|
+
openedBy: z.string().min(1),
|
|
67
|
+
question: z.string().min(1),
|
|
68
|
+
/** 올린 쪽이 사유라고 적은 것. predicate가 아니어서 막혔다. */
|
|
69
|
+
claimedReasons: z.array(z.string()).default([]),
|
|
70
|
+
reason: z.enum(['APPROVAL_NOT_JUSTIFIED', 'DUPLICATE_EPISODE', 'RESUBMIT_NOT_JUSTIFIED']),
|
|
71
|
+
detail: z.string().min(1),
|
|
72
|
+
at: z.string().min(1),
|
|
73
|
+
});
|
|
74
|
+
const recordKey = (id) => `escalation:rec:${id}`;
|
|
75
|
+
const RECORD_PREFIX = 'escalation:rec:';
|
|
76
|
+
const resolvedKey = (id) => `escalation:res:${id}`;
|
|
77
|
+
const rejectedKey = (seq) => `escalation:vio:${String(seq).padStart(6, '0')}`;
|
|
78
|
+
const REJECTED_PREFIX = 'escalation:vio:';
|
|
79
|
+
/**
|
|
80
|
+
* 같은 경계인가 (C-13 §5).
|
|
81
|
+
*
|
|
82
|
+
* **evidence는 넣지 않는다** (불변식 ⑦). 넣으면 근거 한 줄만 더 붙여 같은 질문을 다시
|
|
83
|
+
* 올릴 수 있고, 그게 정확히 Approval Budget이 막으려는 것이다.
|
|
84
|
+
*/
|
|
85
|
+
export function boundaryFingerprint(input) {
|
|
86
|
+
const canonical = JSON.stringify({
|
|
87
|
+
predicates: [...input.predicates].sort(),
|
|
88
|
+
blockedNodes: [...input.blockedNodes].sort(),
|
|
89
|
+
blockedScope: [...(input.blockedScope ?? [])].sort(),
|
|
90
|
+
});
|
|
91
|
+
return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
|
|
92
|
+
}
|
|
93
|
+
export class EscalationLedger {
|
|
94
|
+
#scope;
|
|
95
|
+
#now;
|
|
96
|
+
constructor(scope, now = () => new Date().toISOString()) {
|
|
97
|
+
this.#scope = scope;
|
|
98
|
+
this.#now = now;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 상신을 연다 — **이 함수가 Gate다.**
|
|
102
|
+
*
|
|
103
|
+
* 통과하면 record를 남기고 호출자가 ApprovalRequest를 만든다. 막히면 request는
|
|
104
|
+
* 만들어지지 않고 막힌 사실이 남는다.
|
|
105
|
+
*/
|
|
106
|
+
async open(input) {
|
|
107
|
+
const at = input.openedAt ?? this.#now();
|
|
108
|
+
const valid = input.predicates.filter((candidate) => EscalationPredicate.safeParse(candidate).success);
|
|
109
|
+
if (valid.length === 0) {
|
|
110
|
+
// 불확실성은 경계가 아니다 (C-13 §1.2). 막되, 무엇을 올리려 했는지 남긴다.
|
|
111
|
+
const detail = input.predicates.length > 0
|
|
112
|
+
? `올릴 자격이 되는 사유가 없다 (받은 사유: ${input.predicates.join(', ')})`
|
|
113
|
+
: '올릴 자격이 되는 사유가 하나도 없다';
|
|
114
|
+
await this.#reject(input, 'APPROVAL_NOT_JUSTIFIED', detail, at);
|
|
115
|
+
return { ok: false, reason: 'APPROVAL_NOT_JUSTIFIED', detail };
|
|
116
|
+
}
|
|
117
|
+
if (input.evidenceRefs === undefined || input.evidenceRefs.length === 0) {
|
|
118
|
+
const detail = '근거 없는 상신은 상신이 아니다 — evidence를 최소 하나 대라';
|
|
119
|
+
await this.#reject(input, 'APPROVAL_NOT_JUSTIFIED', detail, at);
|
|
120
|
+
return { ok: false, reason: 'APPROVAL_NOT_JUSTIFIED', detail };
|
|
121
|
+
}
|
|
122
|
+
if (input.blockedNodes.length === 0) {
|
|
123
|
+
return { ok: false, reason: 'INVALID_INPUT', detail: '무엇이 막혔는지(blockedNodes)가 없다' };
|
|
124
|
+
}
|
|
125
|
+
const fingerprint = boundaryFingerprint({
|
|
126
|
+
predicates: valid,
|
|
127
|
+
blockedNodes: input.blockedNodes,
|
|
128
|
+
...(input.blockedScope ? { blockedScope: input.blockedScope } : {}),
|
|
129
|
+
});
|
|
130
|
+
const open = (await this.pending()).filter((record) => record.sessionId === input.sessionId);
|
|
131
|
+
const sameBoundary = open.find((record) => record.boundaryFingerprint === fingerprint);
|
|
132
|
+
if (sameBoundary) {
|
|
133
|
+
const detail = `같은 경계가 이미 열려 있다 (${sameBoundary.escalationId}) — 표현을 바꿔 다시 올리지 않는다`;
|
|
134
|
+
await this.#reject(input, 'DUPLICATE_EPISODE', detail, at);
|
|
135
|
+
return { ok: false, reason: 'DUPLICATE_EPISODE', detail, existing: sameBoundary };
|
|
136
|
+
}
|
|
137
|
+
if (input.previousEscalationId) {
|
|
138
|
+
const refused = await this.#judgeResubmit(input, valid, fingerprint);
|
|
139
|
+
if (refused) {
|
|
140
|
+
await this.#reject(input, 'RESUBMIT_NOT_JUSTIFIED', refused, at);
|
|
141
|
+
return { ok: false, reason: 'RESUBMIT_NOT_JUSTIFIED', detail: refused };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const blocked = new Set(input.blockedNodes);
|
|
145
|
+
const record = EscalationRecord.parse({
|
|
146
|
+
escalationId: input.escalationId,
|
|
147
|
+
sessionId: input.sessionId,
|
|
148
|
+
openedBy: input.openedBy,
|
|
149
|
+
predicates: valid,
|
|
150
|
+
question: input.question,
|
|
151
|
+
evidenceRefs: input.evidenceRefs,
|
|
152
|
+
affectedNodes: input.affectedNodes ?? [],
|
|
153
|
+
blockedNodes: input.blockedNodes,
|
|
154
|
+
blockedScope: input.blockedScope ?? [],
|
|
155
|
+
// 자동 계산이다 — 올리는 쪽이 "다 막혔다"고 적어 전체를 세우는 길을 두지 않는다
|
|
156
|
+
stillRunnableNodes: (input.doneCriteria ?? []).filter((node) => !blocked.has(node)),
|
|
157
|
+
boundaryFingerprint: fingerprint,
|
|
158
|
+
...(input.previousEscalationId ? { previousEscalationId: input.previousEscalationId } : {}),
|
|
159
|
+
...(input.whyPreviousDecisionDoesNotCoverThis
|
|
160
|
+
? { whyPreviousDecisionDoesNotCoverThis: input.whyPreviousDecisionDoesNotCoverThis }
|
|
161
|
+
: {}),
|
|
162
|
+
// 결정 표면은 기존 Approval이다. 여기서는 그 참조만 든다.
|
|
163
|
+
requestId: 'pending',
|
|
164
|
+
openedAt: at,
|
|
165
|
+
});
|
|
166
|
+
if (!(await this.#scope.setIfAbsent(recordKey(record.escalationId), JSON.stringify(record)))) {
|
|
167
|
+
return { ok: false, reason: 'INVALID_INPUT', detail: `${record.escalationId} 는 이미 있다` };
|
|
168
|
+
}
|
|
169
|
+
return { ok: true, record };
|
|
170
|
+
}
|
|
171
|
+
/** 만들어진 ApprovalRequest를 잇는다. record는 이미 확정됐고 참조만 채운다. */
|
|
172
|
+
async attachRequest(escalationId, requestId) {
|
|
173
|
+
const record = await this.#read(recordKey(escalationId));
|
|
174
|
+
if (!record)
|
|
175
|
+
return false;
|
|
176
|
+
await this.#scope.set(recordKey(escalationId), JSON.stringify({ ...record, requestId }));
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 사람이 결정해 닫혔다. **여기서 결정을 대신하지 않는다** — 결정은 기존 Approval
|
|
181
|
+
* 경로에서 일어나고, 이 마커는 "그 결정이 이 경계를 덮었다"는 사실만 남긴다.
|
|
182
|
+
*/
|
|
183
|
+
async resolve(escalationId, decidedBy, decisionRef, at) {
|
|
184
|
+
const record = await this.#read(recordKey(escalationId));
|
|
185
|
+
if (!record)
|
|
186
|
+
return { ok: false, reason: 'NOT_FOUND', detail: `${escalationId} 를 찾지 못했다` };
|
|
187
|
+
const marker = { decidedBy, decisionRef, resolvedAt: at ?? this.#now() };
|
|
188
|
+
if (!(await this.#scope.setIfAbsent(resolvedKey(escalationId), JSON.stringify(marker)))) {
|
|
189
|
+
return { ok: false, reason: 'ALREADY_RESOLVED', detail: `${escalationId} 는 이미 닫혔다` };
|
|
190
|
+
}
|
|
191
|
+
return { ok: true, record };
|
|
192
|
+
}
|
|
193
|
+
/** 아직 사람이 결정하지 않은 상신. 이것이 곧 "무엇을 기다리는가"다. */
|
|
194
|
+
async pending() {
|
|
195
|
+
const out = [];
|
|
196
|
+
for (const record of await this.all()) {
|
|
197
|
+
if (!(await this.#scope.get(resolvedKey(record.escalationId))))
|
|
198
|
+
out.push(record);
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
async all() {
|
|
203
|
+
const keys = (await this.#scope.keys(RECORD_PREFIX)).sort();
|
|
204
|
+
const out = [];
|
|
205
|
+
for (const key of keys) {
|
|
206
|
+
const record = await this.#read(key);
|
|
207
|
+
if (record)
|
|
208
|
+
out.push(record);
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
async get(escalationId) {
|
|
213
|
+
return this.#read(recordKey(escalationId));
|
|
214
|
+
}
|
|
215
|
+
/** 막힌 상신들. Gate가 실제로 무엇을 막았는지 사람이 볼 수 있어야 한다. */
|
|
216
|
+
async rejected() {
|
|
217
|
+
const keys = (await this.#scope.keys(REJECTED_PREFIX)).sort();
|
|
218
|
+
const out = [];
|
|
219
|
+
for (const key of keys) {
|
|
220
|
+
const raw = await this.#scope.get(key);
|
|
221
|
+
if (!raw)
|
|
222
|
+
continue;
|
|
223
|
+
const parsed = RejectedEscalation.safeParse(JSON.parse(raw));
|
|
224
|
+
if (parsed.success)
|
|
225
|
+
out.push(parsed.data);
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* 재상신이 정당한가 (C-13 §5.1). 정당하면 null, 아니면 거절 사유.
|
|
231
|
+
*
|
|
232
|
+
* **전부 필요하다.** evidence 하나 더 붙이는 것은 같은 질문을 더 잘 설명한 것이지
|
|
233
|
+
* 새 질문이 아니다 (불변식 ⑧).
|
|
234
|
+
*/
|
|
235
|
+
async #judgeResubmit(input, valid, fingerprint) {
|
|
236
|
+
const previous = await this.#read(recordKey(input.previousEscalationId));
|
|
237
|
+
if (!previous)
|
|
238
|
+
return `${input.previousEscalationId} 를 찾지 못했다 — 잇는 대상이 없다`;
|
|
239
|
+
if (previous.boundaryFingerprint === fingerprint) {
|
|
240
|
+
return '경계가 앞선 상신과 같다 — 근거만 더해서는 다시 올리지 않는다';
|
|
241
|
+
}
|
|
242
|
+
const newPredicate = valid.some((predicate) => !previous.predicates.includes(predicate));
|
|
243
|
+
const newBoundary = input.blockedNodes.some((node) => !previous.blockedNodes.includes(node)) ||
|
|
244
|
+
(input.blockedScope ?? []).some((scope) => !previous.blockedScope.includes(scope));
|
|
245
|
+
if (!newPredicate && !newBoundary) {
|
|
246
|
+
return '새 predicate도 새 경계도 없다 — 같은 사안이다';
|
|
247
|
+
}
|
|
248
|
+
const newEvidence = (input.evidenceRefs ?? []).some((ref) => !previous.evidenceRefs.includes(ref));
|
|
249
|
+
if (!newEvidence)
|
|
250
|
+
return '이전에 없던 근거가 없다';
|
|
251
|
+
if (!input.whyPreviousDecisionDoesNotCoverThis) {
|
|
252
|
+
return '앞선 결정이 왜 이걸 덮지 못하는지 적지 않았다';
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
async #reject(input, reason, detail, at) {
|
|
257
|
+
const record = RejectedEscalation.parse({
|
|
258
|
+
sessionId: input.sessionId,
|
|
259
|
+
openedBy: input.openedBy,
|
|
260
|
+
question: input.question,
|
|
261
|
+
claimedReasons: [...input.predicates],
|
|
262
|
+
reason,
|
|
263
|
+
detail,
|
|
264
|
+
at,
|
|
265
|
+
});
|
|
266
|
+
let seq = (await this.#scope.keys(REJECTED_PREFIX)).length + 1;
|
|
267
|
+
while (!(await this.#scope.setIfAbsent(rejectedKey(seq), JSON.stringify(record))))
|
|
268
|
+
seq += 1;
|
|
269
|
+
}
|
|
270
|
+
async #read(key) {
|
|
271
|
+
const raw = await this.#scope.get(key);
|
|
272
|
+
if (!raw)
|
|
273
|
+
return null;
|
|
274
|
+
const parsed = EscalationRecord.safeParse(JSON.parse(raw));
|
|
275
|
+
return parsed.success ? parsed.data : null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* 지금 무엇이 막혔고 무엇이 갈 수 있는가 (C-13 §6).
|
|
280
|
+
*
|
|
281
|
+
* **입력은 상신 기록뿐이다.** Checkpoint.blockers 같은 서술 문자열을 읽지 않는다
|
|
282
|
+
* (불변식 ⑤) — 문구를 바꾸는 것이 곧 권한 변경이 되면 안 된다.
|
|
283
|
+
*
|
|
284
|
+
* 결과는 `deriveExecutionState`(C-10 §7)가 그대로 소비한다. 새 상태 enum도, 새 그래프도
|
|
285
|
+
* 만들지 않는다: node는 Done Criteria 항목이고 막힌 것만 빠진다.
|
|
286
|
+
*/
|
|
287
|
+
export function proceedGateFacts(pending, doneCriteria) {
|
|
288
|
+
const blocked = new Set(pending.flatMap((record) => record.blockedNodes));
|
|
289
|
+
const runnable = doneCriteria.filter((node) => !blocked.has(node));
|
|
290
|
+
return {
|
|
291
|
+
// 무엇을 왜 기다리는지 — 사유 없이 "대기"만 있으면 사람이 할 일을 모른다
|
|
292
|
+
waitingOn: pending.map((record) => `${record.escalationId} [${record.predicates.join(', ')}] ${record.blockedNodes.join(', ')}`),
|
|
293
|
+
// 일부만 막혔으면 제한된 범위에서 계속 간다 (Conditional의 근거)
|
|
294
|
+
conditions: blocked.size > 0 && runnable.length > 0 ? [`still runnable: ${runnable.join(', ')}`] : [],
|
|
295
|
+
runnable,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* 사람이 읽는 줄. **왜 이것이 Human Boundary인지가 먼저 온다** (C-13 §7).
|
|
300
|
+
* "어떻게 할까요"가 아니라 "이 경계라서 당신 몫이다"가 상신의 형태다.
|
|
301
|
+
*/
|
|
302
|
+
export function escalationLines(records) {
|
|
303
|
+
if (records.length === 0)
|
|
304
|
+
return [];
|
|
305
|
+
const lines = ['Needs an outside decision:'];
|
|
306
|
+
for (const record of records) {
|
|
307
|
+
lines.push(` ${record.escalationId} — ${record.question}`);
|
|
308
|
+
for (const predicate of record.predicates)
|
|
309
|
+
lines.push(` · ${predicate}`);
|
|
310
|
+
lines.push(` blocked: ${record.blockedNodes.join(', ')}`);
|
|
311
|
+
// 무엇이 막혔는지(node)와 어느 경계에서 막혔는지(scope)는 다른 사실이다.
|
|
312
|
+
// scope가 안 보이면 결정하는 사람이 "어디까지가 남의 것인가"를 다시 물어야 한다.
|
|
313
|
+
if (record.blockedScope.length > 0)
|
|
314
|
+
lines.push(` boundary: ${record.blockedScope.join(', ')}`);
|
|
315
|
+
if (record.stillRunnableNodes.length > 0) {
|
|
316
|
+
// 무엇이 계속 가는지 같이 보여야 "전부 멈췄다"로 읽히지 않는다
|
|
317
|
+
lines.push(` still running: ${record.stillRunnableNodes.join(', ')}`);
|
|
318
|
+
}
|
|
319
|
+
lines.push(` evidence: ${record.evidenceRefs.join(', ')}`);
|
|
320
|
+
}
|
|
321
|
+
return lines;
|
|
322
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export declare const EXECUTION_STATES: readonly ["Ready", "Conditional", "Waiting", "Blocked", "Done"];
|
|
2
|
+
export type ExecutionState = (typeof EXECUTION_STATES)[number];
|
|
3
|
+
/**
|
|
4
|
+
* 파생의 입력. **아는 것만 넣는다** — 모르는 항목은 `undefined` 이고, 그것은 `false` 와
|
|
5
|
+
* 다르다. 모르는 것을 안 된다로 읽으면 멀쩡한 일이 Blocked가 된다.
|
|
6
|
+
*/
|
|
7
|
+
export type ExecutionFacts = {
|
|
8
|
+
/** 필수 전제가 깨졌다. 이유가 있어야 한다 — 이유 없는 Blocked는 진단이 아니다. */
|
|
9
|
+
blockers?: readonly string[];
|
|
10
|
+
/** 외부 결정·이벤트·자격을 기다린다. */
|
|
11
|
+
waitingOn?: readonly string[];
|
|
12
|
+
/** 제한된 범위에서는 갈 수 있다. 무엇이 제한인지 적는다. */
|
|
13
|
+
conditions?: readonly string[];
|
|
14
|
+
/** 선언한 완료 조건. 비어 있으면 Done을 말할 근거가 없다. */
|
|
15
|
+
doneCriteria?: readonly string[];
|
|
16
|
+
/** 그중 충족된 것. */
|
|
17
|
+
metCriteria?: readonly string[];
|
|
18
|
+
/** 요구된 검증이 끝났는가. 요구가 없으면 undefined. */
|
|
19
|
+
verificationPassed?: boolean;
|
|
20
|
+
/** 외부 도구가 말하는 상태. **증거일 뿐 결론이 아니다.** */
|
|
21
|
+
externalState?: string;
|
|
22
|
+
};
|
|
23
|
+
export type ExecutionVerdict = {
|
|
24
|
+
state: ExecutionState;
|
|
25
|
+
/** 왜 그 상태인가. 상태만 주면 사람이 무엇을 해야 할지 모른다. */
|
|
26
|
+
reasons: string[];
|
|
27
|
+
/** 판정에 쓰이지 않은 외부 상태. 있는 그대로 옮긴다. */
|
|
28
|
+
externalState?: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* 지금 이 일이 어디 있는가.
|
|
32
|
+
*
|
|
33
|
+
* 순서가 곧 우선순위다. 막힌 것이 먼저고, 기다리는 것이 그다음이며, Done은 **근거가
|
|
34
|
+
* 다 찼을 때만** 나온다 — 완료를 쉽게 말하는 것이 가장 비싼 오판이다.
|
|
35
|
+
*/
|
|
36
|
+
export declare function deriveExecutionState(facts: ExecutionFacts): ExecutionVerdict;
|
|
37
|
+
/**
|
|
38
|
+
* 사람이 읽는 줄.
|
|
39
|
+
*
|
|
40
|
+
* 외부 상태를 **함께 보이되 결론과 섞지 않는다** — 같은 줄에 두면 "저쪽 도구가 그렇다니까"로
|
|
41
|
+
* 읽히고, 그게 이 모듈이 막으려는 것이다.
|
|
42
|
+
*/
|
|
43
|
+
export declare function executionLine(verdict: ExecutionVerdict, label?: string): string[];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Derived Execution State — 외부 도구의 상태가 아니라 실행의 상태 (C-10 §7).
|
|
2
|
+
//
|
|
3
|
+
// 외부 작업 추적기가 `진행 중` 이라고 말한다고 지금 착수할 수 있는 것이 아니다. 자격이 없을 수도,
|
|
4
|
+
// 정본이 흔들렸을 수도, 앞선 일이 안 끝났을 수도 있다. **외부 work tracker의 상태는
|
|
5
|
+
// 입력 증거일 뿐 실행 상태의 정본이 아니다** (불변식 ⑭).
|
|
6
|
+
//
|
|
7
|
+
// 그래서 저장하지 않고 파생한다. Session·Request 상태 enum은 동결이고(OM §11.2),
|
|
8
|
+
// 여기서 만드는 것은 **읽을 때 계산되는 뷰**다 (불변식 ⑮). 같은 증거에서는 같은 값이
|
|
9
|
+
// 나와야 한다 — 결정성이 없으면 "어제는 Ready였는데"가 설명되지 않는다.
|
|
10
|
+
export const EXECUTION_STATES = ['Ready', 'Conditional', 'Waiting', 'Blocked', 'Done'];
|
|
11
|
+
/**
|
|
12
|
+
* 지금 이 일이 어디 있는가.
|
|
13
|
+
*
|
|
14
|
+
* 순서가 곧 우선순위다. 막힌 것이 먼저고, 기다리는 것이 그다음이며, Done은 **근거가
|
|
15
|
+
* 다 찼을 때만** 나온다 — 완료를 쉽게 말하는 것이 가장 비싼 오판이다.
|
|
16
|
+
*/
|
|
17
|
+
export function deriveExecutionState(facts) {
|
|
18
|
+
const reasons = [];
|
|
19
|
+
const external = facts.externalState ? { externalState: facts.externalState } : {};
|
|
20
|
+
if (facts.blockers && facts.blockers.length > 0) {
|
|
21
|
+
return { state: 'Blocked', reasons: facts.blockers.map((b) => `blocked: ${b}`), ...external };
|
|
22
|
+
}
|
|
23
|
+
// Done은 선언한 조건이 있고, 그것이 다 찼고, 요구된 검증이 통과했을 때만이다.
|
|
24
|
+
const criteria = facts.doneCriteria ?? [];
|
|
25
|
+
const met = new Set(facts.metCriteria ?? []);
|
|
26
|
+
const unmet = criteria.filter((item) => !met.has(item));
|
|
27
|
+
if (criteria.length > 0 && unmet.length === 0) {
|
|
28
|
+
if (facts.verificationPassed === false) {
|
|
29
|
+
return { state: 'Waiting', reasons: ['criteria are met but verification has not passed'], ...external };
|
|
30
|
+
}
|
|
31
|
+
if (facts.verificationPassed === undefined && facts.waitingOn && facts.waitingOn.length > 0) {
|
|
32
|
+
return { state: 'Waiting', reasons: facts.waitingOn.map((w) => `waiting on: ${w}`), ...external };
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
state: 'Done',
|
|
36
|
+
reasons: [
|
|
37
|
+
`${criteria.length} done-criteria met`,
|
|
38
|
+
...(facts.verificationPassed ? ['required verification passed'] : ['no verification required']),
|
|
39
|
+
],
|
|
40
|
+
...external,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// 기다리는 것이 있어도 **갈 수 있는 것이 남아 있으면 멈춘 게 아니다** (C-13 §6).
|
|
44
|
+
// 이 둘을 합쳐 Waiting으로 읽으면 외부 대기 하나가 전체를 세운 것처럼 보인다.
|
|
45
|
+
if (facts.waitingOn && facts.waitingOn.length > 0 && facts.conditions && facts.conditions.length > 0) {
|
|
46
|
+
return {
|
|
47
|
+
state: 'Conditional',
|
|
48
|
+
reasons: [...facts.conditions.map((c) => `limit: ${c}`), ...facts.waitingOn.map((w) => `waiting on: ${w}`)],
|
|
49
|
+
...external,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (facts.waitingOn && facts.waitingOn.length > 0) {
|
|
53
|
+
return { state: 'Waiting', reasons: facts.waitingOn.map((w) => `waiting on: ${w}`), ...external };
|
|
54
|
+
}
|
|
55
|
+
if (facts.conditions && facts.conditions.length > 0) {
|
|
56
|
+
return {
|
|
57
|
+
state: 'Conditional',
|
|
58
|
+
reasons: facts.conditions.map((c) => `limit: ${c}`),
|
|
59
|
+
...external,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (unmet.length > 0)
|
|
63
|
+
reasons.push(`${unmet.length} done-criteria remaining`);
|
|
64
|
+
reasons.push('nothing blocking, nothing to wait on');
|
|
65
|
+
return { state: 'Ready', reasons, ...external };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 사람이 읽는 줄.
|
|
69
|
+
*
|
|
70
|
+
* 외부 상태를 **함께 보이되 결론과 섞지 않는다** — 같은 줄에 두면 "저쪽 도구가 그렇다니까"로
|
|
71
|
+
* 읽히고, 그게 이 모듈이 막으려는 것이다.
|
|
72
|
+
*/
|
|
73
|
+
export function executionLine(verdict, label = 'Execution state') {
|
|
74
|
+
const lines = [`${label}: ${verdict.state}`];
|
|
75
|
+
for (const reason of verdict.reasons)
|
|
76
|
+
lines.push(` ${reason}`);
|
|
77
|
+
if (verdict.externalState) {
|
|
78
|
+
lines.push(` (external tool says: ${verdict.externalState} — evidence, not the basis of this verdict)`);
|
|
79
|
+
}
|
|
80
|
+
return lines;
|
|
81
|
+
}
|