@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,101 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ScopedStore } from '../../ports/state-store.ts';
|
|
3
|
+
/**
|
|
4
|
+
* 이 행위를 지금 해도 되는가.
|
|
5
|
+
*
|
|
6
|
+
* ALLOW 그대로 한다
|
|
7
|
+
* DENY 하지 않는다. 다시 시도할 대상도 아니다 (읽기는 얼려도 다시 밀리지 않는다)
|
|
8
|
+
* DEFER 지금은 못 하지만 **해야 할 일이다** — 큐에 남겨 녹은 뒤 사람이 다시 본다
|
|
9
|
+
*/
|
|
10
|
+
export declare const FreezeDecision: z.ZodEnum<["ALLOW", "DENY", "DEFER"]>;
|
|
11
|
+
export type FreezeDecision = z.infer<typeof FreezeDecision>;
|
|
12
|
+
/** 행위의 갈래. provider 이름이 아니라 **무엇을 하는가**로 가른다. */
|
|
13
|
+
export declare const RemoteAction: z.ZodEnum<["remote.read", "remote.write", "local.inspect", "local.implement", "local.test"]>;
|
|
14
|
+
export type RemoteAction = z.infer<typeof RemoteAction>;
|
|
15
|
+
export declare const FreezePolicy: z.ZodObject<{
|
|
16
|
+
frozen: z.ZodDefault<z.ZodBoolean>;
|
|
17
|
+
/** 언제부터. 얼린 시점을 모르면 무엇이 그 뒤에 쌓였는지도 모른다. */
|
|
18
|
+
since: z.ZodOptional<z.ZodString>;
|
|
19
|
+
/** 왜 얼렸는가. 이유 없는 freeze는 다음 사람이 언제 녹여도 되는지 모른다. */
|
|
20
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
21
|
+
/**
|
|
22
|
+
* 원격 **읽기**까지 막을 것인가. 기본은 허용이다 — 읽기까지 막으면 조사 자체가 서지 않고,
|
|
23
|
+
* 대개 문제가 되는 것은 쓰기다. 완전 오프라인이면 명시적으로 켠다.
|
|
24
|
+
*/
|
|
25
|
+
denyRemoteRead: z.ZodDefault<z.ZodBoolean>;
|
|
26
|
+
}, "strip", z.ZodTypeAny, {
|
|
27
|
+
frozen: boolean;
|
|
28
|
+
denyRemoteRead: boolean;
|
|
29
|
+
reason?: string | undefined;
|
|
30
|
+
since?: string | undefined;
|
|
31
|
+
}, {
|
|
32
|
+
reason?: string | undefined;
|
|
33
|
+
frozen?: boolean | undefined;
|
|
34
|
+
since?: string | undefined;
|
|
35
|
+
denyRemoteRead?: boolean | undefined;
|
|
36
|
+
}>;
|
|
37
|
+
export type FreezePolicy = z.infer<typeof FreezePolicy>;
|
|
38
|
+
export declare const DeferredAction: z.ZodObject<{
|
|
39
|
+
id: z.ZodString;
|
|
40
|
+
action: z.ZodEnum<["remote.read", "remote.write", "local.inspect", "local.implement", "local.test"]>;
|
|
41
|
+
/** 무엇을 하려 했는가. 사람이 읽고 다시 판단할 만큼은 남는다. */
|
|
42
|
+
intent: z.ZodString;
|
|
43
|
+
/** 그때 무엇을 근거로 했는가 — 녹은 뒤 그 근거가 아직 유효한지 대조할 대상이다. */
|
|
44
|
+
basis: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
45
|
+
deferredAt: z.ZodString;
|
|
46
|
+
/** 승인·Grant가 걸려 있던 것이면 그 참조. 자동 재사용하지 않는다. */
|
|
47
|
+
grantRef: z.ZodOptional<z.ZodString>;
|
|
48
|
+
}, "strip", z.ZodTypeAny, {
|
|
49
|
+
id: string;
|
|
50
|
+
action: "remote.read" | "remote.write" | "local.inspect" | "local.implement" | "local.test";
|
|
51
|
+
intent: string;
|
|
52
|
+
basis: string[];
|
|
53
|
+
deferredAt: string;
|
|
54
|
+
grantRef?: string | undefined;
|
|
55
|
+
}, {
|
|
56
|
+
id: string;
|
|
57
|
+
action: "remote.read" | "remote.write" | "local.inspect" | "local.implement" | "local.test";
|
|
58
|
+
intent: string;
|
|
59
|
+
deferredAt: string;
|
|
60
|
+
basis?: string[] | undefined;
|
|
61
|
+
grantRef?: string | undefined;
|
|
62
|
+
}>;
|
|
63
|
+
export type DeferredAction = z.infer<typeof DeferredAction>;
|
|
64
|
+
/**
|
|
65
|
+
* 이 행위를 지금 해도 되는가 (지시 §27).
|
|
66
|
+
*
|
|
67
|
+
* **로컬은 얼리지 않는다.** 구현·테스트·조사가 멈추면 freeze가 곧 작업 중단이 되고,
|
|
68
|
+
* 그러면 사람이 freeze를 안 쓴다 — 안 쓰이는 안전장치는 안전장치가 아니다.
|
|
69
|
+
*/
|
|
70
|
+
export declare function judgeAction(policy: FreezePolicy, action: RemoteAction): {
|
|
71
|
+
decision: FreezeDecision;
|
|
72
|
+
detail: string;
|
|
73
|
+
};
|
|
74
|
+
export declare class FreezeLedger {
|
|
75
|
+
#private;
|
|
76
|
+
constructor(scope: ScopedStore, now?: () => string);
|
|
77
|
+
policy(): Promise<FreezePolicy>;
|
|
78
|
+
/** 얼린다. 이유를 요구한다 — 이유 없는 freeze는 언제 녹여도 되는지 아무도 모른다. */
|
|
79
|
+
freeze(reason: string, options?: {
|
|
80
|
+
denyRemoteRead?: boolean;
|
|
81
|
+
}): Promise<FreezePolicy>;
|
|
82
|
+
/**
|
|
83
|
+
* 녹인다. **쌓인 것을 자동으로 실행하지 않는다** (지시 §27).
|
|
84
|
+
*
|
|
85
|
+
* 미뤄 둔 것을 그대로 돌려주기만 한다. 그 사이 대상이 바뀌었을 수 있고 승인 근거가
|
|
86
|
+
* 낡았을 수 있으므로, 다시 확인하고 다시 승인하는 것은 사람의 일이다.
|
|
87
|
+
*/
|
|
88
|
+
thaw(): Promise<{
|
|
89
|
+
policy: FreezePolicy;
|
|
90
|
+
deferred: DeferredAction[];
|
|
91
|
+
}>;
|
|
92
|
+
/** 미뤄 둔 것을 남긴다. 같은 id를 덮어쓰지 않는다 — 미룬 사실도 기록이다. */
|
|
93
|
+
defer(input: Omit<DeferredAction, 'deferredAt'> & {
|
|
94
|
+
deferredAt?: string;
|
|
95
|
+
}): Promise<boolean>;
|
|
96
|
+
deferred(): Promise<DeferredAction[]>;
|
|
97
|
+
/** 사람이 다시 판단해 처리했다. **여기서 실행하지 않는다** — 실행은 기존 Grant 경로다. */
|
|
98
|
+
release(id: string): Promise<boolean>;
|
|
99
|
+
}
|
|
100
|
+
/** 사람이 읽는 줄. 녹인 뒤 무엇을 다시 봐야 하는지가 이 목록이다. */
|
|
101
|
+
export declare function freezeLines(policy: FreezePolicy, deferred: readonly DeferredAction[]): string[];
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Remote Freeze — 원격을 얼려도 로컬 일은 계속 돈다 (C-10 §7 계열 · 지시 §27).
|
|
2
|
+
//
|
|
3
|
+
// 실전 근거: 프로젝트 운영 중 "지금은 원격에 아무것도 보내지 마라"는 상태가 실제로 있다.
|
|
4
|
+
// 그때 필요한 것은 두 가지다.
|
|
5
|
+
//
|
|
6
|
+
// ① 원격 쓰기가 **정말로** 안 나가야 한다 — "조심하겠다"는 약속이 아니라 차단이어야 한다
|
|
7
|
+
// ② 로컬 작업은 계속 돌아야 한다 — 얼렸다고 구현·테스트까지 멈추면 아무도 안 쓴다
|
|
8
|
+
//
|
|
9
|
+
// 그리고 녹일 때가 더 위험하다. 얼어 있는 동안 쌓인 것을 **자동으로 재생하지 않는다**:
|
|
10
|
+
// 그 사이 대상이 바뀌었을 수 있고, 승인 근거가 낡았을 수 있다. 다시 확인하고 다시 승인한다.
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
/**
|
|
13
|
+
* 이 행위를 지금 해도 되는가.
|
|
14
|
+
*
|
|
15
|
+
* ALLOW 그대로 한다
|
|
16
|
+
* DENY 하지 않는다. 다시 시도할 대상도 아니다 (읽기는 얼려도 다시 밀리지 않는다)
|
|
17
|
+
* DEFER 지금은 못 하지만 **해야 할 일이다** — 큐에 남겨 녹은 뒤 사람이 다시 본다
|
|
18
|
+
*/
|
|
19
|
+
export const FreezeDecision = z.enum(['ALLOW', 'DENY', 'DEFER']);
|
|
20
|
+
/** 행위의 갈래. provider 이름이 아니라 **무엇을 하는가**로 가른다. */
|
|
21
|
+
export const RemoteAction = z.enum([
|
|
22
|
+
'remote.read',
|
|
23
|
+
'remote.write',
|
|
24
|
+
'local.inspect',
|
|
25
|
+
'local.implement',
|
|
26
|
+
'local.test',
|
|
27
|
+
]);
|
|
28
|
+
export const FreezePolicy = z.object({
|
|
29
|
+
frozen: z.boolean().default(false),
|
|
30
|
+
/** 언제부터. 얼린 시점을 모르면 무엇이 그 뒤에 쌓였는지도 모른다. */
|
|
31
|
+
since: z.string().optional(),
|
|
32
|
+
/** 왜 얼렸는가. 이유 없는 freeze는 다음 사람이 언제 녹여도 되는지 모른다. */
|
|
33
|
+
reason: z.string().optional(),
|
|
34
|
+
/**
|
|
35
|
+
* 원격 **읽기**까지 막을 것인가. 기본은 허용이다 — 읽기까지 막으면 조사 자체가 서지 않고,
|
|
36
|
+
* 대개 문제가 되는 것은 쓰기다. 완전 오프라인이면 명시적으로 켠다.
|
|
37
|
+
*/
|
|
38
|
+
denyRemoteRead: z.boolean().default(false),
|
|
39
|
+
});
|
|
40
|
+
export const DeferredAction = z.object({
|
|
41
|
+
id: z.string().min(1),
|
|
42
|
+
action: RemoteAction,
|
|
43
|
+
/** 무엇을 하려 했는가. 사람이 읽고 다시 판단할 만큼은 남는다. */
|
|
44
|
+
intent: z.string().min(1),
|
|
45
|
+
/** 그때 무엇을 근거로 했는가 — 녹은 뒤 그 근거가 아직 유효한지 대조할 대상이다. */
|
|
46
|
+
basis: z.array(z.string()).default([]),
|
|
47
|
+
deferredAt: z.string().min(1),
|
|
48
|
+
/** 승인·Grant가 걸려 있던 것이면 그 참조. 자동 재사용하지 않는다. */
|
|
49
|
+
grantRef: z.string().optional(),
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* 이 행위를 지금 해도 되는가 (지시 §27).
|
|
53
|
+
*
|
|
54
|
+
* **로컬은 얼리지 않는다.** 구현·테스트·조사가 멈추면 freeze가 곧 작업 중단이 되고,
|
|
55
|
+
* 그러면 사람이 freeze를 안 쓴다 — 안 쓰이는 안전장치는 안전장치가 아니다.
|
|
56
|
+
*/
|
|
57
|
+
export function judgeAction(policy, action) {
|
|
58
|
+
if (!policy.frozen)
|
|
59
|
+
return { decision: 'ALLOW', detail: '얼지 않았다' };
|
|
60
|
+
switch (action) {
|
|
61
|
+
case 'remote.write':
|
|
62
|
+
// 해야 할 일이지만 지금은 아니다 — 버리지 않고 미룬다
|
|
63
|
+
return { decision: 'DEFER', detail: `원격 쓰기는 얼어 있다${policy.reason ? ` (${policy.reason})` : ''}` };
|
|
64
|
+
case 'remote.read':
|
|
65
|
+
return policy.denyRemoteRead
|
|
66
|
+
? { decision: 'DENY', detail: '완전 오프라인 — 원격 읽기도 막혀 있다' }
|
|
67
|
+
: { decision: 'ALLOW', detail: '읽기는 허용된다 — 막히는 것은 쓰기다' };
|
|
68
|
+
default:
|
|
69
|
+
return { decision: 'ALLOW', detail: '로컬 작업은 얼리지 않는다' };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const POLICY_KEY = 'freeze-policy';
|
|
73
|
+
const deferredKey = (id) => `freeze-deferred:${id}`;
|
|
74
|
+
const DEFERRED_PREFIX = 'freeze-deferred:';
|
|
75
|
+
export class FreezeLedger {
|
|
76
|
+
#scope;
|
|
77
|
+
#now;
|
|
78
|
+
constructor(scope, now = () => new Date().toISOString()) {
|
|
79
|
+
this.#scope = scope;
|
|
80
|
+
this.#now = now;
|
|
81
|
+
}
|
|
82
|
+
async policy() {
|
|
83
|
+
const raw = await this.#scope.get(POLICY_KEY);
|
|
84
|
+
return raw ? FreezePolicy.parse(JSON.parse(raw)) : FreezePolicy.parse({});
|
|
85
|
+
}
|
|
86
|
+
/** 얼린다. 이유를 요구한다 — 이유 없는 freeze는 언제 녹여도 되는지 아무도 모른다. */
|
|
87
|
+
async freeze(reason, options = {}) {
|
|
88
|
+
const policy = FreezePolicy.parse({
|
|
89
|
+
frozen: true,
|
|
90
|
+
since: this.#now(),
|
|
91
|
+
reason,
|
|
92
|
+
denyRemoteRead: options.denyRemoteRead ?? false,
|
|
93
|
+
});
|
|
94
|
+
await this.#scope.set(POLICY_KEY, JSON.stringify(policy));
|
|
95
|
+
return policy;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 녹인다. **쌓인 것을 자동으로 실행하지 않는다** (지시 §27).
|
|
99
|
+
*
|
|
100
|
+
* 미뤄 둔 것을 그대로 돌려주기만 한다. 그 사이 대상이 바뀌었을 수 있고 승인 근거가
|
|
101
|
+
* 낡았을 수 있으므로, 다시 확인하고 다시 승인하는 것은 사람의 일이다.
|
|
102
|
+
*/
|
|
103
|
+
async thaw() {
|
|
104
|
+
const policy = FreezePolicy.parse({ frozen: false });
|
|
105
|
+
await this.#scope.set(POLICY_KEY, JSON.stringify(policy));
|
|
106
|
+
return { policy, deferred: await this.deferred() };
|
|
107
|
+
}
|
|
108
|
+
/** 미뤄 둔 것을 남긴다. 같은 id를 덮어쓰지 않는다 — 미룬 사실도 기록이다. */
|
|
109
|
+
async defer(input) {
|
|
110
|
+
const record = DeferredAction.parse({ ...input, deferredAt: input.deferredAt ?? this.#now() });
|
|
111
|
+
return this.#scope.setIfAbsent(deferredKey(record.id), JSON.stringify(record));
|
|
112
|
+
}
|
|
113
|
+
async deferred() {
|
|
114
|
+
const keys = (await this.#scope.keys(DEFERRED_PREFIX)).sort();
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const key of keys) {
|
|
117
|
+
const raw = await this.#scope.get(key);
|
|
118
|
+
if (!raw)
|
|
119
|
+
continue;
|
|
120
|
+
const parsed = DeferredAction.safeParse(JSON.parse(raw));
|
|
121
|
+
if (parsed.success)
|
|
122
|
+
out.push(parsed.data);
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
/** 사람이 다시 판단해 처리했다. **여기서 실행하지 않는다** — 실행은 기존 Grant 경로다. */
|
|
127
|
+
async release(id) {
|
|
128
|
+
if (!(await this.#scope.get(deferredKey(id))))
|
|
129
|
+
return false;
|
|
130
|
+
await this.#scope.delete(deferredKey(id));
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** 사람이 읽는 줄. 녹인 뒤 무엇을 다시 봐야 하는지가 이 목록이다. */
|
|
135
|
+
export function freezeLines(policy, deferred) {
|
|
136
|
+
const lines = [];
|
|
137
|
+
lines.push(policy.frozen
|
|
138
|
+
? `원격 얼림 (${policy.since ?? '시점 미상'})${policy.reason ? ` — ${policy.reason}` : ''}`
|
|
139
|
+
: '원격 얼림 없음');
|
|
140
|
+
if (policy.frozen && policy.denyRemoteRead)
|
|
141
|
+
lines.push(' 읽기도 막혀 있다 (완전 오프라인)');
|
|
142
|
+
if (deferred.length > 0) {
|
|
143
|
+
lines.push(`미뤄 둔 것 ${deferred.length}건 — 녹은 뒤 자동으로 나가지 않는다. 다시 확인하고 승인하라:`);
|
|
144
|
+
for (const item of deferred) {
|
|
145
|
+
lines.push(` ${item.id} [${item.action}] ${item.intent} (${item.deferredAt})`);
|
|
146
|
+
if (item.basis.length > 0)
|
|
147
|
+
lines.push(` 당시 근거: ${item.basis.join(', ')} — 아직 유효한지 보라`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return lines;
|
|
151
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ParsedScope = {
|
|
2
|
+
kind: 'all';
|
|
3
|
+
} | {
|
|
4
|
+
kind: 'recursive';
|
|
5
|
+
segments: string[];
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'children';
|
|
8
|
+
segments: string[];
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'exact';
|
|
11
|
+
segments: string[];
|
|
12
|
+
};
|
|
13
|
+
/** 문법 밖 패턴은 null이다 — 호출자는 이를 위반으로 다뤄야 하며, 통과시켜서는 안 된다. */
|
|
14
|
+
export declare function parseScope(scope: string): ParsedScope | null;
|
|
15
|
+
/** 실제 경로가 이 범위들 중 하나에 드는가. */
|
|
16
|
+
export declare function pathInScope(path: string, scopes: readonly string[]): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* sub가 가리키는 경로 집합이 sup 안에 전부 들어가는가.
|
|
19
|
+
* 판정 불가(문법 밖)면 false — 모르면 거부한다.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isScopeSubset(sub: string, sup: string): boolean;
|
|
22
|
+
/** 여러 상위 범위 중 하나라도 sub를 품으면 통과다. */
|
|
23
|
+
export declare function isWithinScopes(sub: string, supers: readonly string[]): boolean;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// ASC scope grammar와 두 가지 판정.
|
|
2
|
+
//
|
|
3
|
+
// 이 둘은 다른 질문이고 다른 답을 준다. 섞으면 권한이 새어 나간다:
|
|
4
|
+
// pathInScope — "이 파일을 써도 되는가" (경로 대 패턴)
|
|
5
|
+
// isScopeSubset — "이 범위가 저 범위 안에 드는가" (패턴 대 패턴, 집합 포함)
|
|
6
|
+
//
|
|
7
|
+
// glob 매처로 패턴 대 패턴을 판정하면 안 된다. `node:path`의 matchesGlob으로
|
|
8
|
+
// `frontend/**`가 `frontend/*`의 부분집합인지 물으면 true가 나온다 — `**`가 리터럴처럼
|
|
9
|
+
// 매칭되기 때문이다. 재귀 범위가 1단계 범위 안에 든다는 답은 곧 권한 확장 통과다.
|
|
10
|
+
// 그래서 문법을 좁히고 집합 포함을 직접 계산한다.
|
|
11
|
+
//
|
|
12
|
+
// 허용 문법 (이외는 전부 거부):
|
|
13
|
+
// ** 모든 경로
|
|
14
|
+
// <prefix>/** prefix 이하 전부 (재귀)
|
|
15
|
+
// <prefix>/* prefix의 직속 자식만 (1단계)
|
|
16
|
+
// <path> 정확히 그 경로 하나
|
|
17
|
+
/** 문법 밖 패턴은 null이다 — 호출자는 이를 위반으로 다뤄야 하며, 통과시켜서는 안 된다. */
|
|
18
|
+
export function parseScope(scope) {
|
|
19
|
+
if (scope.length === 0)
|
|
20
|
+
return null;
|
|
21
|
+
if (scope === '**')
|
|
22
|
+
return { kind: 'all' };
|
|
23
|
+
const segments = scope.split('/');
|
|
24
|
+
const last = segments.at(-1);
|
|
25
|
+
const head = segments.slice(0, -1);
|
|
26
|
+
// 와일드카드는 마지막 세그먼트에만, 그리고 그 세그먼트 전체여야 한다.
|
|
27
|
+
// 'frontend/*/studio'나 'src/*.ts'는 집합 포함을 단순 계산할 수 없어 거부한다.
|
|
28
|
+
if (head.some((s) => s.includes('*') || s.length === 0))
|
|
29
|
+
return null;
|
|
30
|
+
if (last === '**')
|
|
31
|
+
return head.length > 0 ? { kind: 'recursive', segments: head } : null;
|
|
32
|
+
if (last === '*')
|
|
33
|
+
return head.length > 0 ? { kind: 'children', segments: head } : null;
|
|
34
|
+
if (last.includes('*') || last.length === 0)
|
|
35
|
+
return null;
|
|
36
|
+
return { kind: 'exact', segments };
|
|
37
|
+
}
|
|
38
|
+
const startsWith = (path, prefix) => prefix.length <= path.length && prefix.every((segment, i) => path[i] === segment);
|
|
39
|
+
const sameSegments = (a, b) => a.length === b.length && a.every((segment, i) => segment === b[i]);
|
|
40
|
+
/** 실제 경로가 이 범위들 중 하나에 드는가. */
|
|
41
|
+
export function pathInScope(path, scopes) {
|
|
42
|
+
const segments = path.split('/');
|
|
43
|
+
return scopes.some((scope) => {
|
|
44
|
+
const parsed = parseScope(scope);
|
|
45
|
+
if (!parsed)
|
|
46
|
+
return false; // 잘못 쓴 범위는 아무것도 허용하지 않는다
|
|
47
|
+
switch (parsed.kind) {
|
|
48
|
+
case 'all':
|
|
49
|
+
return true;
|
|
50
|
+
case 'recursive':
|
|
51
|
+
return startsWith(segments, parsed.segments) && segments.length > parsed.segments.length;
|
|
52
|
+
case 'children':
|
|
53
|
+
return startsWith(segments, parsed.segments) && segments.length === parsed.segments.length + 1;
|
|
54
|
+
case 'exact':
|
|
55
|
+
return sameSegments(segments, parsed.segments);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* sub가 가리키는 경로 집합이 sup 안에 전부 들어가는가.
|
|
61
|
+
* 판정 불가(문법 밖)면 false — 모르면 거부한다.
|
|
62
|
+
*/
|
|
63
|
+
export function isScopeSubset(sub, sup) {
|
|
64
|
+
const a = parseScope(sub);
|
|
65
|
+
const b = parseScope(sup);
|
|
66
|
+
if (!a || !b)
|
|
67
|
+
return false;
|
|
68
|
+
if (b.kind === 'all')
|
|
69
|
+
return true;
|
|
70
|
+
if (a.kind === 'all')
|
|
71
|
+
return false; // 전체는 어떤 부분범위에도 들어가지 않는다
|
|
72
|
+
switch (b.kind) {
|
|
73
|
+
case 'recursive':
|
|
74
|
+
if (!startsWith(a.segments, b.segments))
|
|
75
|
+
return false;
|
|
76
|
+
// `p/**`는 p 아래를 뜻하지 p 자신이 아니다 — pathInScope도 같은 규칙이므로,
|
|
77
|
+
// exact 'frontend'가 'frontend/**'의 부분집합이 되면 두 판정이 어긋난다.
|
|
78
|
+
return a.kind === 'exact' ? a.segments.length > b.segments.length : true;
|
|
79
|
+
case 'children':
|
|
80
|
+
// 1단계만 허용하는 범위에 재귀 범위는 들어갈 수 없다 — 이것이 matchesGlob이 놓친 지점이다.
|
|
81
|
+
if (a.kind === 'recursive')
|
|
82
|
+
return false;
|
|
83
|
+
if (a.kind === 'children')
|
|
84
|
+
return sameSegments(a.segments, b.segments);
|
|
85
|
+
return a.segments.length === b.segments.length + 1 && startsWith(a.segments, b.segments);
|
|
86
|
+
case 'exact':
|
|
87
|
+
return a.kind === 'exact' && sameSegments(a.segments, b.segments);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** 여러 상위 범위 중 하나라도 sub를 품으면 통과다. */
|
|
91
|
+
export function isWithinScopes(sub, supers) {
|
|
92
|
+
return supers.some((sup) => isScopeSubset(sub, sup));
|
|
93
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type HealthAlert } from '../monitor/health-alerts.ts';
|
|
3
|
+
import type { DecisionSummary } from '../view/decision-view.ts';
|
|
4
|
+
import type { DigestBatch, DeliveryOutcome, PresentationPort } from '../../ports/presentation.ts';
|
|
5
|
+
import type { ScopedStore } from '../../ports/state-store.ts';
|
|
6
|
+
export type DigestInput = {
|
|
7
|
+
at: string;
|
|
8
|
+
/** 판단 대기 중인 것들. LocalOperator가 이미 우선순위·freshness를 붙여 준다. */
|
|
9
|
+
pending: readonly DecisionSummary[];
|
|
10
|
+
/** 숨겨 두고 보는 것의 수 (C-07 §5). 숫자만 알려도 무엇을 못 보고 있는지 안다. */
|
|
11
|
+
shadowCount?: number;
|
|
12
|
+
/** 회수 경로에서 발견된 수. coverage 정보이며 우선순위와 무관하다 (C-07 §1.6). */
|
|
13
|
+
recoveredCount?: number;
|
|
14
|
+
/** 이미 보낸 것 — requestId → 그때 version. 같은 것을 두 번 묶어 보내지 않는다. */
|
|
15
|
+
delivered?: ReadonlyMap<string, number>;
|
|
16
|
+
/**
|
|
17
|
+
* 감시 자체의 경고 (C-12 §3). Core가 판정하지 않는다 — 판정은 evaluateHealth가 하고
|
|
18
|
+
* 여기는 그 결과를 함께 실어 나른다.
|
|
19
|
+
*/
|
|
20
|
+
health?: readonly HealthAlert[];
|
|
21
|
+
};
|
|
22
|
+
export type DigestPlan = {
|
|
23
|
+
/** 지금 끊어야 하는 것. P0만이다. */
|
|
24
|
+
urgent: DecisionSummary[];
|
|
25
|
+
/** 묶어서 건넬 것. */
|
|
26
|
+
batch: DigestBatch;
|
|
27
|
+
/** 보내지 않은 것과 그 이유. 조용히 빠지면 사람이 왜 안 보였는지 알 수 없다. */
|
|
28
|
+
skipped: {
|
|
29
|
+
alreadyDecided: number;
|
|
30
|
+
alreadyDelivered: number;
|
|
31
|
+
shadow: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* 감시 자체의 상태 (C-12 §3). 판단 요청이 아니라 **"지금 이 목록을 믿어도 되는가"** 다.
|
|
35
|
+
* 비어 있지 않으면 목록이 조용한 이유가 조용해서가 아닐 수 있다.
|
|
36
|
+
*/
|
|
37
|
+
health: HealthAlert[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* 무엇을 지금 건네고 무엇을 묶을지 정한다.
|
|
41
|
+
*
|
|
42
|
+
* 간격·주기는 여기 없다. 그건 설정이고 실행 계기는 밖에 있다 (C-08 §2.3) — 프로젝트마다
|
|
43
|
+
* 다르고 같은 프로젝트에서도 시기마다 다르다.
|
|
44
|
+
*/
|
|
45
|
+
export declare function planDigest(input: DigestInput): DigestPlan;
|
|
46
|
+
/**
|
|
47
|
+
* 사람이 먼저 봐야 할 것은 **몇 건이 있고 그중 지금 정해야 할 것이 무엇인가**다 (C-08 §2.4).
|
|
48
|
+
* 걸러진 것도 숫자로 남긴다 — 0으로 감추면 걸러졌다는 사실 자체가 사라진다.
|
|
49
|
+
*/
|
|
50
|
+
export declare function renderDigest(plan: DigestPlan): string[];
|
|
51
|
+
export declare const Delivery: z.ZodObject<{
|
|
52
|
+
requestId: z.ZodString;
|
|
53
|
+
version: z.ZodNumber;
|
|
54
|
+
channel: z.ZodString;
|
|
55
|
+
externalRef: z.ZodOptional<z.ZodString>;
|
|
56
|
+
at: z.ZodString;
|
|
57
|
+
}, "strip", z.ZodTypeAny, {
|
|
58
|
+
at: string;
|
|
59
|
+
version: number;
|
|
60
|
+
channel: string;
|
|
61
|
+
requestId: string;
|
|
62
|
+
externalRef?: string | undefined;
|
|
63
|
+
}, {
|
|
64
|
+
at: string;
|
|
65
|
+
version: number;
|
|
66
|
+
channel: string;
|
|
67
|
+
requestId: string;
|
|
68
|
+
externalRef?: string | undefined;
|
|
69
|
+
}>;
|
|
70
|
+
export type Delivery = z.infer<typeof Delivery>;
|
|
71
|
+
/**
|
|
72
|
+
* 어느 request를 어느 채널에 언제 보냈는가.
|
|
73
|
+
*
|
|
74
|
+
* **Core Entity가 아니다** (C-01 §9, C-02 §3) — 정본이 아니고, 전달은 best-effort이며,
|
|
75
|
+
* 실패가 canonical state에 영향을 주면 안 된다. 그래서 adapter-scope에 둔다.
|
|
76
|
+
*/
|
|
77
|
+
export declare class DeliveryLedger {
|
|
78
|
+
#private;
|
|
79
|
+
constructor(scope: ScopedStore, now?: () => string);
|
|
80
|
+
record(requestId: string, version: number, channel: string, externalRef?: string): Promise<void>;
|
|
81
|
+
/** 이 채널에 무엇을 어느 판으로 보냈는가. */
|
|
82
|
+
delivered(channel: string): Promise<Map<string, number>>;
|
|
83
|
+
}
|
|
84
|
+
export type DeliveryReport = {
|
|
85
|
+
channel: string;
|
|
86
|
+
digest?: DeliveryOutcome;
|
|
87
|
+
urgent: {
|
|
88
|
+
requestId: string;
|
|
89
|
+
outcome: DeliveryOutcome;
|
|
90
|
+
}[];
|
|
91
|
+
/** 능력이 없어 아래로 내린 것. 조용히 무시하지 않는다 (C-08 §1.3). */
|
|
92
|
+
degraded: string[];
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* 건넨다. 채널이 능력을 다 갖추지 못하면 **그 기능만 내려간다** — 급한 것을 따로 보낼 수
|
|
96
|
+
* 없으면 묶음에 실어 보내고, 그 사실을 적는다.
|
|
97
|
+
*/
|
|
98
|
+
export declare function deliver(plan: DigestPlan, channel: PresentationPort, ledger?: DeliveryLedger): Promise<DeliveryReport>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Digest Coordinator — 감지와 방해를 분리한다 (C-08).
|
|
2
|
+
//
|
|
3
|
+
// 빨리 감지하는 것과 사람을 지금 끊는 것은 다른 결정이다. 지금까지 둘이 붙어 있었고, 그래서
|
|
4
|
+
// 실시간 감지를 켜면 새 사건마다 작업 흐름이 끊겼다. 문제는 감지 속도가 아니라 전달 방식이다.
|
|
5
|
+
//
|
|
6
|
+
// **Monitor 밖이다.** Monitor의 write 경계는 자기 계약 파일·inbox entity·log뿐이고(OM §10.7),
|
|
7
|
+
// 전달은 그 밖이다. 여기서 패킷을 만들지 않는다 — 이미 만들어진 것을 언제 어디로 건넬지만 정한다.
|
|
8
|
+
//
|
|
9
|
+
// 패킷 생성은 미루지 않는다. 미루면 그 사건은 재시도 대기로 남아 다음 회차가 같은 조사를
|
|
10
|
+
// 다시 하게 되고, 미완료와 보류가 구분되지 않는다 (C-08 §2.2).
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { healthAlertLines } from "../monitor/health-alerts.js";
|
|
13
|
+
/**
|
|
14
|
+
* 무엇을 지금 건네고 무엇을 묶을지 정한다.
|
|
15
|
+
*
|
|
16
|
+
* 간격·주기는 여기 없다. 그건 설정이고 실행 계기는 밖에 있다 (C-08 §2.3) — 프로젝트마다
|
|
17
|
+
* 다르고 같은 프로젝트에서도 시기마다 다르다.
|
|
18
|
+
*/
|
|
19
|
+
export function planDigest(input) {
|
|
20
|
+
const urgent = [];
|
|
21
|
+
const p1 = [];
|
|
22
|
+
const p2 = [];
|
|
23
|
+
const health = input.health ?? [];
|
|
24
|
+
const skipped = { alreadyDecided: 0, alreadyDelivered: 0, shadow: input.shadowCount ?? 0 };
|
|
25
|
+
for (const item of input.pending) {
|
|
26
|
+
// 이미 결정된 것은 판단 요청이 아니다. freshness가 그 사실을 들고 있다 (C-01 §7).
|
|
27
|
+
if (item.freshness === 'ALREADY_DECIDED') {
|
|
28
|
+
skipped.alreadyDecided++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// 같은 판(version)을 다시 보내지 않는다. 바뀌었으면 다시 보낸다 — 그건 새 사실이다.
|
|
32
|
+
if (input.delivered?.get(item.requestId) === item.version) {
|
|
33
|
+
skipped.alreadyDelivered++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (item.priority === 'P0')
|
|
37
|
+
urgent.push(item);
|
|
38
|
+
else if (item.priority === 'P1')
|
|
39
|
+
p1.push(item);
|
|
40
|
+
else
|
|
41
|
+
p2.push(item);
|
|
42
|
+
}
|
|
43
|
+
const groups = [
|
|
44
|
+
...(urgent.length > 0 ? [{ priority: 'P0', items: urgent }] : []),
|
|
45
|
+
...(p1.length > 0 ? [{ priority: 'P1', items: p1 }] : []),
|
|
46
|
+
...(p2.length > 0 ? [{ priority: 'P2', items: p2 }] : []),
|
|
47
|
+
];
|
|
48
|
+
return {
|
|
49
|
+
health: [...health],
|
|
50
|
+
urgent,
|
|
51
|
+
batch: {
|
|
52
|
+
at: input.at,
|
|
53
|
+
groups,
|
|
54
|
+
suppressed: { shadow: skipped.shadow, alreadyDecided: skipped.alreadyDecided },
|
|
55
|
+
...(input.recoveredCount ? { recovered: input.recoveredCount } : {}),
|
|
56
|
+
},
|
|
57
|
+
skipped,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const LABEL = { P0: '🔴 지금 확인 필요', P1: '🟡 판단 필요', P2: '🔵 참고' };
|
|
61
|
+
/**
|
|
62
|
+
* 사람이 먼저 봐야 할 것은 **몇 건이 있고 그중 지금 정해야 할 것이 무엇인가**다 (C-08 §2.4).
|
|
63
|
+
* 걸러진 것도 숫자로 남긴다 — 0으로 감추면 걸러졌다는 사실 자체가 사라진다.
|
|
64
|
+
*/
|
|
65
|
+
export function renderDigest(plan) {
|
|
66
|
+
const lines = [`ASC · ${plan.batch.at}`];
|
|
67
|
+
for (const group of plan.batch.groups) {
|
|
68
|
+
lines.push(`${LABEL[group.priority] ?? group.priority} ${group.items.length}`);
|
|
69
|
+
for (const item of group.items) {
|
|
70
|
+
lines.push(` ${item.requestId} ${item.reference} ${item.title}`);
|
|
71
|
+
// 묶은 뒤 변한 것은 새 값을 만들지 않고 freshness 4종으로 말한다 (C-08 §3.3).
|
|
72
|
+
if (item.freshness !== 'CURRENT')
|
|
73
|
+
lines.push(` (${item.freshness})`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// 목록보다 먼저 온다. 감시가 고장 났으면 "건넬 것이 없다"는 사실이 아니다.
|
|
77
|
+
for (const line of healthAlertLines(plan.health))
|
|
78
|
+
lines.push(line);
|
|
79
|
+
if (plan.skipped.shadow > 0)
|
|
80
|
+
lines.push(`⚪ 숨김 ${plan.skipped.shadow} (관련 근거 없음 — 계속 본다)`);
|
|
81
|
+
if (plan.skipped.alreadyDecided > 0)
|
|
82
|
+
lines.push(`⚪ 이미 결정됨 ${plan.skipped.alreadyDecided}`);
|
|
83
|
+
if (plan.skipped.alreadyDelivered > 0)
|
|
84
|
+
lines.push(`⚪ 이미 보냄 ${plan.skipped.alreadyDelivered}`);
|
|
85
|
+
if (plan.batch.recovered)
|
|
86
|
+
lines.push(`↺ 회수 경로에서 발견 ${plan.batch.recovered}`);
|
|
87
|
+
if (plan.batch.groups.length === 0) {
|
|
88
|
+
// "변경 없음"과 "못 봄"을 합치지 않는다 (C-12 불변식 ⑫)
|
|
89
|
+
lines.push(plan.health.length > 0 ? '건넬 것이 없다 — 다만 위 경고를 먼저 보라.' : '건넬 것이 없다.');
|
|
90
|
+
}
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
93
|
+
// ── 전달 기록 ────────────────────────────────────────────────────────────────
|
|
94
|
+
export const Delivery = z.object({
|
|
95
|
+
requestId: z.string().min(1),
|
|
96
|
+
version: z.number().int().nonnegative(),
|
|
97
|
+
channel: z.string().min(1),
|
|
98
|
+
externalRef: z.string().optional(),
|
|
99
|
+
at: z.string().min(1),
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* 어느 request를 어느 채널에 언제 보냈는가.
|
|
103
|
+
*
|
|
104
|
+
* **Core Entity가 아니다** (C-01 §9, C-02 §3) — 정본이 아니고, 전달은 best-effort이며,
|
|
105
|
+
* 실패가 canonical state에 영향을 주면 안 된다. 그래서 adapter-scope에 둔다.
|
|
106
|
+
*/
|
|
107
|
+
export class DeliveryLedger {
|
|
108
|
+
#scope;
|
|
109
|
+
#now;
|
|
110
|
+
constructor(scope, now = () => new Date().toISOString()) {
|
|
111
|
+
this.#scope = scope;
|
|
112
|
+
this.#now = now;
|
|
113
|
+
}
|
|
114
|
+
async record(requestId, version, channel, externalRef) {
|
|
115
|
+
await this.#scope.set(`sent:${channel}:${requestId}`, JSON.stringify(Delivery.parse({ requestId, version, channel, ...(externalRef ? { externalRef } : {}), at: this.#now() })));
|
|
116
|
+
}
|
|
117
|
+
/** 이 채널에 무엇을 어느 판으로 보냈는가. */
|
|
118
|
+
async delivered(channel) {
|
|
119
|
+
const out = new Map();
|
|
120
|
+
for (const key of await this.#scope.keys(`sent:${channel}:`)) {
|
|
121
|
+
const raw = await this.#scope.get(key);
|
|
122
|
+
if (raw) {
|
|
123
|
+
const record = Delivery.parse(JSON.parse(raw));
|
|
124
|
+
out.set(record.requestId, record.version);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 건넨다. 채널이 능력을 다 갖추지 못하면 **그 기능만 내려간다** — 급한 것을 따로 보낼 수
|
|
132
|
+
* 없으면 묶음에 실어 보내고, 그 사실을 적는다.
|
|
133
|
+
*/
|
|
134
|
+
export async function deliver(plan, channel, ledger) {
|
|
135
|
+
const report = { channel: channel.id, urgent: [], degraded: [] };
|
|
136
|
+
if (channel.capabilities.has('presentation.priority') && channel.presentUrgent) {
|
|
137
|
+
for (const item of plan.urgent) {
|
|
138
|
+
const outcome = await channel.presentUrgent(item);
|
|
139
|
+
report.urgent.push({ requestId: item.requestId, outcome });
|
|
140
|
+
if (outcome.ok)
|
|
141
|
+
await ledger?.record(item.requestId, item.version, channel.id, outcome.externalRef);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if (plan.urgent.length > 0) {
|
|
145
|
+
report.degraded.push(`${channel.id} 는 급한 건을 따로 전달하지 못한다 — ${plan.urgent.length}건을 묶음에 실었다`);
|
|
146
|
+
}
|
|
147
|
+
if (!channel.capabilities.has('presentation.digest')) {
|
|
148
|
+
report.degraded.push(`${channel.id} 는 묶음을 전달하지 못한다`);
|
|
149
|
+
return report;
|
|
150
|
+
}
|
|
151
|
+
report.digest = await channel.presentDigest(plan.batch);
|
|
152
|
+
if (report.digest.ok && ledger) {
|
|
153
|
+
for (const group of plan.batch.groups) {
|
|
154
|
+
for (const item of group.items) {
|
|
155
|
+
await ledger.record(item.requestId, item.version, channel.id, report.digest.externalRef);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return report;
|
|
160
|
+
}
|