@asc-agent/runtime 0.4.0 → 0.5.1
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/README.md +1 -1
- package/dist/adapters/claude-code/guard.js +26 -1
- package/dist/adapters/claude-code/install.d.ts +27 -0
- package/dist/adapters/claude-code/install.js +3 -1
- package/dist/adapters/fixture-surface/index.d.ts +33 -0
- package/dist/adapters/fixture-surface/index.js +97 -0
- package/dist/adapters/gitlab/adapter.js +1 -0
- package/dist/adapters/gitlab/client.d.ts +16 -2
- package/dist/adapters/gitlab/client.js +27 -0
- package/dist/adapters/gitlab/coordination.d.ts +19 -0
- package/dist/adapters/gitlab/coordination.js +111 -0
- package/dist/adapters/gitlab/ports.d.ts +6 -0
- package/dist/adapters/gitlab/ports.js +29 -2
- package/dist/adapters/local/canonical.d.ts +20 -0
- package/dist/adapters/local/canonical.js +58 -0
- package/dist/adapters/markdown/state-store.js +2 -1
- package/dist/adapters/service/launchd.js +18 -1
- package/dist/adapters/service/systemd-user.js +4 -1
- package/dist/adapters/webhook/ingress.d.ts +2 -2
- package/dist/cli/asc.js +569 -17
- package/dist/composition/runtime.d.ts +20 -0
- package/dist/composition/runtime.js +32 -1
- package/dist/core/attach/setup-plan.d.ts +107 -6
- package/dist/core/attach/setup-plan.js +104 -6
- package/dist/core/attach/setup.d.ts +3 -1
- package/dist/core/attach/setup.js +25 -1
- package/dist/core/binding/types.d.ts +1 -1
- package/dist/core/binding/types.js +1 -0
- package/dist/core/distribution/external-command.d.ts +26 -0
- package/dist/core/distribution/external-command.js +58 -1
- package/dist/core/distribution/persistent-runtime.d.ts +9 -0
- package/dist/core/distribution/release.d.ts +3 -3
- package/dist/core/distribution/release.js +1 -1
- package/dist/core/distribution/service-runtime.d.ts +40 -0
- package/dist/core/distribution/service-runtime.js +76 -0
- package/dist/core/runtime/coordination.d.ts +312 -0
- package/dist/core/runtime/coordination.js +247 -0
- package/dist/core/runtime/front.d.ts +10 -0
- package/dist/core/runtime/front.js +16 -1
- package/dist/core/runtime/publish.d.ts +107 -0
- package/dist/core/runtime/publish.js +153 -0
- package/dist/core/runtime/workspaces.d.ts +22 -0
- package/dist/core/runtime/workspaces.js +18 -0
- package/dist/ports/coordination-surface.d.ts +59 -0
- package/dist/ports/coordination-surface.js +16 -0
- package/dist/ports/resource-context.d.ts +5 -0
- package/dist/schemas/profile.d.ts +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type NodeCandidate } from './node-runtime.ts';
|
|
2
|
+
/**
|
|
3
|
+
* 사라질 수 있는 자리인가.
|
|
4
|
+
*
|
|
5
|
+
* npx 캐시가 이 목록의 이유다. 임시 디렉터리도 같은 성질이고, 어느 쪽이든 **등록물이
|
|
6
|
+
* 가리키면 안 되는 자리**다. 목록은 짧게 유지한다 — 길어지면 그건 판정이 아니라 추측이다.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isTransientPath(path: string): boolean;
|
|
9
|
+
export type ServiceRuntimeInput = {
|
|
10
|
+
/** 지금 이 프로세스의 진입점 절대 경로. */
|
|
11
|
+
runningEntry: string;
|
|
12
|
+
/** 지금 이 프로세스의 Node 실행 파일. */
|
|
13
|
+
runningNode: string;
|
|
14
|
+
/** 지금 이 프로세스의 Node 버전 (`v24.1.0` 형태). */
|
|
15
|
+
runningNodeVersion: string;
|
|
16
|
+
/** 전역 설치본의 진입점. 있으면 이것이 먼저다 — 지워지지 않는 자리이기 때문이다. */
|
|
17
|
+
stableEntry?: string;
|
|
18
|
+
/** 이 기계에서 찾은 Node 후보들. 지금 Node 가 못 쓸 때만 본다. */
|
|
19
|
+
nodeCandidates?: readonly NodeCandidate[];
|
|
20
|
+
};
|
|
21
|
+
export type ServiceRuntimeResolution = {
|
|
22
|
+
kind: 'STABLE';
|
|
23
|
+
node: string;
|
|
24
|
+
entry: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'UNSTABLE';
|
|
27
|
+
/** 무엇이 없어서 못 하는가. 사람이 읽고 무엇을 할지 알 수 있어야 한다. */
|
|
28
|
+
reason: 'NO_STABLE_ENTRY' | 'NO_COMPATIBLE_NODE';
|
|
29
|
+
detail: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 등록물에 박을 Node 와 진입점을 정한다. **아무것도 실행하지 않는다** — 사실은 호출자가
|
|
33
|
+
* 관측해 넘긴다.
|
|
34
|
+
*
|
|
35
|
+
* 진입점: 전역 설치본이 있으면 그것. 없으면 지금 진입점이되 **임시 자리가 아닐 때만.**
|
|
36
|
+
* Node: 지금 Node 가 하한을 넘고 임시 자리가 아니면 그것. 아니면 후보 중 첫 번째.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveServiceRuntime(input: ServiceRuntimeInput): ServiceRuntimeResolution;
|
|
39
|
+
/** 사람이 읽는 한 줄. 왜 등록하지 않았는지가 여기 있어야 한다. */
|
|
40
|
+
export declare function serviceRuntimeLine(resolution: ServiceRuntimeResolution): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// 등록물이 무엇을 가리켜야 하는가 (C-12 · C-14 §3).
|
|
2
|
+
//
|
|
3
|
+
// 실기계에서 이런 일이 났다: `npx @asc-agent/bootstrap … setup apply` 로 처음 설치한
|
|
4
|
+
// 사람에게, 등록물이 **npx 캐시 안의 실행물**을 정본으로 박았다.
|
|
5
|
+
//
|
|
6
|
+
// ~/.npm/_npx/<hash>/node_modules/@asc-agent/runtime/dist/cli/asc.js
|
|
7
|
+
//
|
|
8
|
+
// npx 캐시는 지워지는 자리다. 그것을 지우면 등록물은 남고 실행물만 사라진다 — OS 는 계속
|
|
9
|
+
// 부르고 매번 실패한다. 등록은 **오래 남을 설치본**이 스스로 할 때만 뜻이 있다.
|
|
10
|
+
//
|
|
11
|
+
// 그래서 이 파일은 두 가지를 가른다:
|
|
12
|
+
//
|
|
13
|
+
// 지금 이 프로세스가 어디서 도는가 bootstrap 은 임시 자리에서 돌 수 있다
|
|
14
|
+
// 등록물이 무엇을 가리켜야 하는가 임시 자리는 절대 안 된다
|
|
15
|
+
//
|
|
16
|
+
// Node 도 같다. 등록물이 박는 Node 는 실제로 있고, 하한을 넘고, 임시 자리가 아니어야
|
|
17
|
+
// 한다 — 셋 중 하나라도 아니면 **등록하지 않는다.** 깨진 등록을 남기고 나중에 실패하는
|
|
18
|
+
// 것보다, 등록하지 않고 그 사실을 말하는 편이 낫다.
|
|
19
|
+
import { MINIMUM_NODE_MAJOR, majorOf } from "./node-runtime.js";
|
|
20
|
+
/**
|
|
21
|
+
* 사라질 수 있는 자리인가.
|
|
22
|
+
*
|
|
23
|
+
* npx 캐시가 이 목록의 이유다. 임시 디렉터리도 같은 성질이고, 어느 쪽이든 **등록물이
|
|
24
|
+
* 가리키면 안 되는 자리**다. 목록은 짧게 유지한다 — 길어지면 그건 판정이 아니라 추측이다.
|
|
25
|
+
*/
|
|
26
|
+
export function isTransientPath(path) {
|
|
27
|
+
const normalized = path.replace(/\\/g, '/');
|
|
28
|
+
return (normalized.includes('/_npx/') ||
|
|
29
|
+
normalized.includes('/_cacache/') ||
|
|
30
|
+
normalized.startsWith('/tmp/') ||
|
|
31
|
+
normalized.startsWith('/private/tmp/') ||
|
|
32
|
+
normalized.startsWith('/var/folders/') ||
|
|
33
|
+
/\/Temp\//i.test(normalized));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 등록물에 박을 Node 와 진입점을 정한다. **아무것도 실행하지 않는다** — 사실은 호출자가
|
|
37
|
+
* 관측해 넘긴다.
|
|
38
|
+
*
|
|
39
|
+
* 진입점: 전역 설치본이 있으면 그것. 없으면 지금 진입점이되 **임시 자리가 아닐 때만.**
|
|
40
|
+
* Node: 지금 Node 가 하한을 넘고 임시 자리가 아니면 그것. 아니면 후보 중 첫 번째.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveServiceRuntime(input) {
|
|
43
|
+
const entry = input.stableEntry && !isTransientPath(input.stableEntry)
|
|
44
|
+
? input.stableEntry
|
|
45
|
+
: !isTransientPath(input.runningEntry)
|
|
46
|
+
? input.runningEntry
|
|
47
|
+
: null;
|
|
48
|
+
if (!entry) {
|
|
49
|
+
return {
|
|
50
|
+
kind: 'UNSTABLE',
|
|
51
|
+
reason: 'NO_STABLE_ENTRY',
|
|
52
|
+
detail: `${input.runningEntry} is a temporary location — a registration pointing there breaks when it is cleared`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const usable = (path, version) => {
|
|
56
|
+
const major = majorOf(version);
|
|
57
|
+
return major !== null && major >= MINIMUM_NODE_MAJOR && !isTransientPath(path);
|
|
58
|
+
};
|
|
59
|
+
const node = usable(input.runningNode, input.runningNodeVersion)
|
|
60
|
+
? input.runningNode
|
|
61
|
+
: (input.nodeCandidates ?? []).find((candidate) => usable(candidate.path, candidate.version))?.path;
|
|
62
|
+
if (!node) {
|
|
63
|
+
return {
|
|
64
|
+
kind: 'UNSTABLE',
|
|
65
|
+
reason: 'NO_COMPATIBLE_NODE',
|
|
66
|
+
detail: `no Node ${MINIMUM_NODE_MAJOR} or newer at a stable path (this process runs ${input.runningNodeVersion} from ${input.runningNode})`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { kind: 'STABLE', node, entry };
|
|
70
|
+
}
|
|
71
|
+
/** 사람이 읽는 한 줄. 왜 등록하지 않았는지가 여기 있어야 한다. */
|
|
72
|
+
export function serviceRuntimeLine(resolution) {
|
|
73
|
+
return resolution.kind === 'STABLE'
|
|
74
|
+
? `service runtime: ${resolution.node} ${resolution.entry}`
|
|
75
|
+
: `service runtime unavailable — ${resolution.detail}`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ScopedStore } from '../../ports/state-store.ts';
|
|
3
|
+
/**
|
|
4
|
+
* 외부 시스템이 준 **안정적인** 신원 (F3).
|
|
5
|
+
*
|
|
6
|
+
* 사람이 보는 링크는 여기 있어도 되지만 그것이 정본은 아니다. 링크의 모양이 예상과
|
|
7
|
+
* 다르다는 이유로 생성이 실패했다고 읽고 하나 더 만든 사고가 실제로 있었다 — 그때
|
|
8
|
+
* 대조할 수 있는 다른 값이 계약에 없었다.
|
|
9
|
+
*/
|
|
10
|
+
export declare const RemoteIdentity: z.ZodObject<{
|
|
11
|
+
/** 어느 adapter 가 이 사실을 만들었는가. provider 이름이 아니라 adapter id 다. */
|
|
12
|
+
adapter: z.ZodString;
|
|
13
|
+
/** 그 adapter 의 어휘로 된 객체 종류. Core 는 이 문자열로 분기하지 않는다. */
|
|
14
|
+
objectType: z.ZodString;
|
|
15
|
+
/** 그 시스템 안에서 이 객체를 다시 찾을 수 있는 값. **성공의 정본이다.** */
|
|
16
|
+
objectId: z.ZodString;
|
|
17
|
+
/** 어느 자원 아래인가 (저장소·프로젝트 등). adapter 어휘 그대로. */
|
|
18
|
+
resource: z.ZodOptional<z.ZodString>;
|
|
19
|
+
/**
|
|
20
|
+
* 사람이 열어 보는 주소. **판정에 쓰지 않는다** — 모양이 바뀌어도 같은 객체다.
|
|
21
|
+
*/
|
|
22
|
+
locator: z.ZodOptional<z.ZodString>;
|
|
23
|
+
/** 이 시점의 판본. 있으면 응답 중복을 가리는 데 쓴다. */
|
|
24
|
+
revisionMarker: z.ZodOptional<z.ZodString>;
|
|
25
|
+
}, "strip", z.ZodTypeAny, {
|
|
26
|
+
adapter: string;
|
|
27
|
+
objectType: string;
|
|
28
|
+
objectId: string;
|
|
29
|
+
resource?: string | undefined;
|
|
30
|
+
revisionMarker?: string | undefined;
|
|
31
|
+
locator?: string | undefined;
|
|
32
|
+
}, {
|
|
33
|
+
adapter: string;
|
|
34
|
+
objectType: string;
|
|
35
|
+
objectId: string;
|
|
36
|
+
resource?: string | undefined;
|
|
37
|
+
revisionMarker?: string | undefined;
|
|
38
|
+
locator?: string | undefined;
|
|
39
|
+
}>;
|
|
40
|
+
export type RemoteIdentity = z.infer<typeof RemoteIdentity>;
|
|
41
|
+
/**
|
|
42
|
+
* 기대가 실제로 밖에 게시됐다는 증거.
|
|
43
|
+
*
|
|
44
|
+
* **이 레코드를 만드는 것이 게시가 아니다.** 게시는 adapter 가 하고, 여기 남는 것은
|
|
45
|
+
* 그것이 일어났다는 관측이다 — 그 구분이 사라지면 "적어 뒀으니 됐다"가 다시 성립한다.
|
|
46
|
+
*/
|
|
47
|
+
export declare const CommunicationEvidence: z.ZodObject<{
|
|
48
|
+
evidenceId: z.ZodString;
|
|
49
|
+
/** 어느 기대에 대한 것인가. 지금은 Bounded Query 가 기대의 정본이다 (C-04). */
|
|
50
|
+
queryId: z.ZodString;
|
|
51
|
+
/** 어느 binding 을 통해 나갔는가 (C-09). 역할 이름이며 provider 이름이 아니다. */
|
|
52
|
+
bindingRole: z.ZodOptional<z.ZodString>;
|
|
53
|
+
identity: z.ZodObject<{
|
|
54
|
+
/** 어느 adapter 가 이 사실을 만들었는가. provider 이름이 아니라 adapter id 다. */
|
|
55
|
+
adapter: z.ZodString;
|
|
56
|
+
/** 그 adapter 의 어휘로 된 객체 종류. Core 는 이 문자열로 분기하지 않는다. */
|
|
57
|
+
objectType: z.ZodString;
|
|
58
|
+
/** 그 시스템 안에서 이 객체를 다시 찾을 수 있는 값. **성공의 정본이다.** */
|
|
59
|
+
objectId: z.ZodString;
|
|
60
|
+
/** 어느 자원 아래인가 (저장소·프로젝트 등). adapter 어휘 그대로. */
|
|
61
|
+
resource: z.ZodOptional<z.ZodString>;
|
|
62
|
+
/**
|
|
63
|
+
* 사람이 열어 보는 주소. **판정에 쓰지 않는다** — 모양이 바뀌어도 같은 객체다.
|
|
64
|
+
*/
|
|
65
|
+
locator: z.ZodOptional<z.ZodString>;
|
|
66
|
+
/** 이 시점의 판본. 있으면 응답 중복을 가리는 데 쓴다. */
|
|
67
|
+
revisionMarker: z.ZodOptional<z.ZodString>;
|
|
68
|
+
}, "strip", z.ZodTypeAny, {
|
|
69
|
+
adapter: string;
|
|
70
|
+
objectType: string;
|
|
71
|
+
objectId: string;
|
|
72
|
+
resource?: string | undefined;
|
|
73
|
+
revisionMarker?: string | undefined;
|
|
74
|
+
locator?: string | undefined;
|
|
75
|
+
}, {
|
|
76
|
+
adapter: string;
|
|
77
|
+
objectType: string;
|
|
78
|
+
objectId: string;
|
|
79
|
+
resource?: string | undefined;
|
|
80
|
+
revisionMarker?: string | undefined;
|
|
81
|
+
locator?: string | undefined;
|
|
82
|
+
}>;
|
|
83
|
+
/** 누구에게 닿아야 하는가. 프로젝트 정책이 정하고 여기서는 그대로 보관한다. */
|
|
84
|
+
audience: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
85
|
+
publishedAt: z.ZodString;
|
|
86
|
+
/** 이 사실을 우리가 언제 봤는가. 게시 시각과 다를 수 있다. */
|
|
87
|
+
observedAt: z.ZodString;
|
|
88
|
+
/** 선언인가 관측인가 (C-10 의 같은 필드와 같은 뜻). */
|
|
89
|
+
evidenceSource: z.ZodString;
|
|
90
|
+
}, "strip", z.ZodTypeAny, {
|
|
91
|
+
observedAt: string;
|
|
92
|
+
identity: {
|
|
93
|
+
adapter: string;
|
|
94
|
+
objectType: string;
|
|
95
|
+
objectId: string;
|
|
96
|
+
resource?: string | undefined;
|
|
97
|
+
revisionMarker?: string | undefined;
|
|
98
|
+
locator?: string | undefined;
|
|
99
|
+
};
|
|
100
|
+
evidenceSource: string;
|
|
101
|
+
evidenceId: string;
|
|
102
|
+
queryId: string;
|
|
103
|
+
audience: string[];
|
|
104
|
+
publishedAt: string;
|
|
105
|
+
bindingRole?: string | undefined;
|
|
106
|
+
}, {
|
|
107
|
+
observedAt: string;
|
|
108
|
+
identity: {
|
|
109
|
+
adapter: string;
|
|
110
|
+
objectType: string;
|
|
111
|
+
objectId: string;
|
|
112
|
+
resource?: string | undefined;
|
|
113
|
+
revisionMarker?: string | undefined;
|
|
114
|
+
locator?: string | undefined;
|
|
115
|
+
};
|
|
116
|
+
evidenceSource: string;
|
|
117
|
+
evidenceId: string;
|
|
118
|
+
queryId: string;
|
|
119
|
+
publishedAt: string;
|
|
120
|
+
bindingRole?: string | undefined;
|
|
121
|
+
audience?: string[] | undefined;
|
|
122
|
+
}>;
|
|
123
|
+
export type CommunicationEvidence = z.infer<typeof CommunicationEvidence>;
|
|
124
|
+
/**
|
|
125
|
+
* 그 게시물에 외부 응답이 도착했다는 증거.
|
|
126
|
+
*
|
|
127
|
+
* **답의 의미를 판정하지 않는다.** 이것은 `QueryAnswer` 가 아니고, 자동으로 그것이
|
|
128
|
+
* 되지도 않는다 — 누가 무엇을 결정할 수 있는지는 C-04 의 권한 판정이고, 사람 승인은
|
|
129
|
+
* C-13 이다. 여기 있는 것은 "왔다"까지다.
|
|
130
|
+
*/
|
|
131
|
+
export declare const ResponseEvidence: z.ZodObject<{
|
|
132
|
+
evidenceId: z.ZodString;
|
|
133
|
+
/** 어느 게시물에 대한 응답인가. */
|
|
134
|
+
communicationId: z.ZodString;
|
|
135
|
+
identity: z.ZodObject<{
|
|
136
|
+
/** 어느 adapter 가 이 사실을 만들었는가. provider 이름이 아니라 adapter id 다. */
|
|
137
|
+
adapter: z.ZodString;
|
|
138
|
+
/** 그 adapter 의 어휘로 된 객체 종류. Core 는 이 문자열로 분기하지 않는다. */
|
|
139
|
+
objectType: z.ZodString;
|
|
140
|
+
/** 그 시스템 안에서 이 객체를 다시 찾을 수 있는 값. **성공의 정본이다.** */
|
|
141
|
+
objectId: z.ZodString;
|
|
142
|
+
/** 어느 자원 아래인가 (저장소·프로젝트 등). adapter 어휘 그대로. */
|
|
143
|
+
resource: z.ZodOptional<z.ZodString>;
|
|
144
|
+
/**
|
|
145
|
+
* 사람이 열어 보는 주소. **판정에 쓰지 않는다** — 모양이 바뀌어도 같은 객체다.
|
|
146
|
+
*/
|
|
147
|
+
locator: z.ZodOptional<z.ZodString>;
|
|
148
|
+
/** 이 시점의 판본. 있으면 응답 중복을 가리는 데 쓴다. */
|
|
149
|
+
revisionMarker: z.ZodOptional<z.ZodString>;
|
|
150
|
+
}, "strip", z.ZodTypeAny, {
|
|
151
|
+
adapter: string;
|
|
152
|
+
objectType: string;
|
|
153
|
+
objectId: string;
|
|
154
|
+
resource?: string | undefined;
|
|
155
|
+
revisionMarker?: string | undefined;
|
|
156
|
+
locator?: string | undefined;
|
|
157
|
+
}, {
|
|
158
|
+
adapter: string;
|
|
159
|
+
objectType: string;
|
|
160
|
+
objectId: string;
|
|
161
|
+
resource?: string | undefined;
|
|
162
|
+
revisionMarker?: string | undefined;
|
|
163
|
+
locator?: string | undefined;
|
|
164
|
+
}>;
|
|
165
|
+
/** 그 시스템이 말하는 응답자. 우리 역할 이름으로 번역하지 않는다. */
|
|
166
|
+
responder: z.ZodOptional<z.ZodString>;
|
|
167
|
+
receivedAt: z.ZodString;
|
|
168
|
+
observedAt: z.ZodString;
|
|
169
|
+
evidenceSource: z.ZodString;
|
|
170
|
+
}, "strip", z.ZodTypeAny, {
|
|
171
|
+
observedAt: string;
|
|
172
|
+
identity: {
|
|
173
|
+
adapter: string;
|
|
174
|
+
objectType: string;
|
|
175
|
+
objectId: string;
|
|
176
|
+
resource?: string | undefined;
|
|
177
|
+
revisionMarker?: string | undefined;
|
|
178
|
+
locator?: string | undefined;
|
|
179
|
+
};
|
|
180
|
+
evidenceSource: string;
|
|
181
|
+
evidenceId: string;
|
|
182
|
+
communicationId: string;
|
|
183
|
+
receivedAt: string;
|
|
184
|
+
responder?: string | undefined;
|
|
185
|
+
}, {
|
|
186
|
+
observedAt: string;
|
|
187
|
+
identity: {
|
|
188
|
+
adapter: string;
|
|
189
|
+
objectType: string;
|
|
190
|
+
objectId: string;
|
|
191
|
+
resource?: string | undefined;
|
|
192
|
+
revisionMarker?: string | undefined;
|
|
193
|
+
locator?: string | undefined;
|
|
194
|
+
};
|
|
195
|
+
evidenceSource: string;
|
|
196
|
+
evidenceId: string;
|
|
197
|
+
communicationId: string;
|
|
198
|
+
receivedAt: string;
|
|
199
|
+
responder?: string | undefined;
|
|
200
|
+
}>;
|
|
201
|
+
export type ResponseEvidence = z.infer<typeof ResponseEvidence>;
|
|
202
|
+
/**
|
|
203
|
+
* 파생 상태. **저장하지 않는다** — 위 증거들에서 매번 계산한다.
|
|
204
|
+
*/
|
|
205
|
+
export type CoordinationState =
|
|
206
|
+
/** 기대는 있는데 밖에 나간 적이 없다. */
|
|
207
|
+
'UNPUBLISHED'
|
|
208
|
+
/** 나갔고, 답을 기다린다. */
|
|
209
|
+
| 'WAITING_EXTERNAL'
|
|
210
|
+
/** 답이 왔다. 그 답이 무엇을 뜻하는지는 여기서 정하지 않는다. */
|
|
211
|
+
| 'RESPONSE_RECEIVED'
|
|
212
|
+
/** 나갔고, 애초에 답을 기대하지 않는다. */
|
|
213
|
+
| 'PUBLISHED';
|
|
214
|
+
export type CoordinationView = {
|
|
215
|
+
queryId: string;
|
|
216
|
+
state: CoordinationState;
|
|
217
|
+
/** 이 기대가 답을 기다리는가. 기대 쪽 사실이며 증거가 아니다. */
|
|
218
|
+
expectsResponse: boolean;
|
|
219
|
+
communications: CommunicationEvidence[];
|
|
220
|
+
responses: ResponseEvidence[];
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* 기대 하나의 지금 상태를 계산한다.
|
|
224
|
+
*
|
|
225
|
+
* 순서가 곧 규칙이다: 응답이 있으면 왔다, 없으면 기다리는지 아닌지, 게시가 없으면
|
|
226
|
+
* 아직 나가지 않았다. **없는 것을 있는 것으로 올리는 방향은 없다.**
|
|
227
|
+
*/
|
|
228
|
+
export declare function deriveCoordination(input: {
|
|
229
|
+
queryId: string;
|
|
230
|
+
expectsResponse: boolean;
|
|
231
|
+
communications: readonly CommunicationEvidence[];
|
|
232
|
+
responses: readonly ResponseEvidence[];
|
|
233
|
+
}): CoordinationView;
|
|
234
|
+
export type AppendOutcome<T> = {
|
|
235
|
+
ok: true;
|
|
236
|
+
evidence: T;
|
|
237
|
+
}
|
|
238
|
+
/** 같은 id 가 이미 있다. 덮지 않는다 — 증거는 한 번만 쓰인다 (C-10 과 같은 이유). */
|
|
239
|
+
| {
|
|
240
|
+
ok: false;
|
|
241
|
+
reason: 'ALREADY_EXISTS';
|
|
242
|
+
detail: string;
|
|
243
|
+
}
|
|
244
|
+
/** 가리키는 게시물이 없다. 없는 것에 응답을 붙이지 않는다. */
|
|
245
|
+
| {
|
|
246
|
+
ok: false;
|
|
247
|
+
reason: 'COMMUNICATION_NOT_FOUND';
|
|
248
|
+
detail: string;
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* 조율 증거 원장. append-only 이며 `setIfAbsent` 위에 선다.
|
|
252
|
+
*
|
|
253
|
+
* 잃는 것이 표시값이 아니라 **누가 무엇을 밖에 내보냈고 누가 답했는가**이기 때문에,
|
|
254
|
+
* 나중 쓰기가 앞선 것을 덮지 않는다.
|
|
255
|
+
*/
|
|
256
|
+
/** 밖에서 본 글 하나. adapter 어휘를 그대로 받되 Core 는 세 가지만 본다. */
|
|
257
|
+
export type ObservedRemark = {
|
|
258
|
+
id: string;
|
|
259
|
+
author: string;
|
|
260
|
+
at: string;
|
|
261
|
+
/** 사람이 쓴 것이 아니라 그 시스템이 남긴 자국인가. */
|
|
262
|
+
system?: boolean;
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* 게시물에 붙은 글 중 **응답으로 셀 것**을 고른다.
|
|
266
|
+
*
|
|
267
|
+
* 두 가지를 뺀다: 그 시스템이 스스로 남긴 자국(커밋 언급·배정 변경)과, 우리가 쓴 글.
|
|
268
|
+
* 둘 다 세면 아무도 답하지 않은 스레드가 답이 온 것으로 보인다 — 이 구조가 막으려는
|
|
269
|
+
* 바로 그 착각이다.
|
|
270
|
+
*
|
|
271
|
+
* **답의 의미는 판정하지 않는다.** 여기서 나오는 것은 "왔다"까지이고, 그것이 결정인지
|
|
272
|
+
* 승인인지는 C-04·C-13 의 몫이다.
|
|
273
|
+
*/
|
|
274
|
+
export declare function responsesFrom(communication: CommunicationEvidence, remarks: readonly ObservedRemark[], mine: ReadonlySet<string>): Omit<ResponseEvidence, 'observedAt'>[];
|
|
275
|
+
export declare class CoordinationLedger {
|
|
276
|
+
#private;
|
|
277
|
+
constructor(scope: ScopedStore, now?: () => string);
|
|
278
|
+
publishRecorded(input: Omit<CommunicationEvidence, 'observedAt'> & {
|
|
279
|
+
observedAt?: string;
|
|
280
|
+
}): Promise<AppendOutcome<CommunicationEvidence>>;
|
|
281
|
+
responseRecorded(input: Omit<ResponseEvidence, 'observedAt'> & {
|
|
282
|
+
observedAt?: string;
|
|
283
|
+
}): Promise<AppendOutcome<ResponseEvidence>>;
|
|
284
|
+
communications(): Promise<CommunicationEvidence[]>;
|
|
285
|
+
responses(): Promise<ResponseEvidence[]>;
|
|
286
|
+
/**
|
|
287
|
+
* 이미 이 게시물이 있는가 — **원격 신원으로** 찾는다 (F3).
|
|
288
|
+
*
|
|
289
|
+
* 같은 것을 두 번 만들지 않기 위한 조회다. 제목이나 링크가 아니라 adapter 가 준
|
|
290
|
+
* 안정 id 로 본다.
|
|
291
|
+
*/
|
|
292
|
+
findByIdentity(identity: Pick<RemoteIdentity, 'adapter' | 'objectType' | 'objectId'>): Promise<CommunicationEvidence | null>;
|
|
293
|
+
/** 기대 하나에 달린 증거 전부. */
|
|
294
|
+
forQuery(queryId: string): Promise<{
|
|
295
|
+
communications: CommunicationEvidence[];
|
|
296
|
+
responses: ResponseEvidence[];
|
|
297
|
+
}>;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* 기대 목록과 증거를 합쳐 지금 상태를 낸다.
|
|
301
|
+
*
|
|
302
|
+
* 기대는 이 모듈이 만들지 않는다 — Bounded Query 가 정본이고, 여기서는 그것을 받아
|
|
303
|
+
* 증거와 맞춘다. 기대가 없으면 조율도 없다.
|
|
304
|
+
*/
|
|
305
|
+
export declare function viewCoordination(ledger: CoordinationLedger, expectations: readonly {
|
|
306
|
+
id: string;
|
|
307
|
+
expectsResponse: boolean;
|
|
308
|
+
}[]): Promise<CoordinationView[]>;
|
|
309
|
+
/**
|
|
310
|
+
* 사람이 읽는 줄. **없는 것을 0 으로 그리지 않는다** — 기대가 없으면 그렇게 말한다.
|
|
311
|
+
*/
|
|
312
|
+
export declare function coordinationLines(views: readonly CoordinationView[]): string[];
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// Coordination Evidence — 물었다는 사실과, 전달됐다는 사실과, 답이 왔다는 사실을 나눈다.
|
|
2
|
+
//
|
|
3
|
+
// 반복된 실패는 이것이었다:
|
|
4
|
+
//
|
|
5
|
+
// dependency 가 있다
|
|
6
|
+
// ≠ 상대에게 전달됐다
|
|
7
|
+
// ≠ 상대가 응답했다
|
|
8
|
+
//
|
|
9
|
+
// 세 사실이 한 레코드로 뭉개지면 "Task 도 있고 무언가 오갔으니 됐다"가 성립한다.
|
|
10
|
+
// 그래서 뭉개지지 않게 **따로 적는다.**
|
|
11
|
+
//
|
|
12
|
+
// 새 subsystem 을 만들지 않는다. 기대는 이미 Bounded Query 가 지고 있고(C-04), 여기서
|
|
13
|
+
// 더하는 것은 C-10 과 같은 모양의 append-only 원장 둘뿐이다:
|
|
14
|
+
//
|
|
15
|
+
// ExecutionEvidence 누가 이 세션을 실제로 집었는가 (C-10, 이미 있음)
|
|
16
|
+
// CommunicationEvidence 그 기대가 실제로 밖에 게시됐는가 (여기)
|
|
17
|
+
// ResponseEvidence 그 게시물에 실제로 답이 왔는가 (여기)
|
|
18
|
+
//
|
|
19
|
+
// 상태를 저장하지 않는다. `UNPUBLISHED`·`WAITING_EXTERNAL`·`RESPONSE_RECEIVED` 는
|
|
20
|
+
// 전부 위 셋에서 **파생**한다 — 저장하면 그것이 곧 두 번째 정본이 되고, 증거와 어긋나는
|
|
21
|
+
// 순간 어느 쪽이 맞는지 정할 방법이 없어진다.
|
|
22
|
+
//
|
|
23
|
+
// **provider 이름을 모른다.** 아는 것은 adapter id 와 그 adapter 가 준 opaque identity 다
|
|
24
|
+
// (C-09 §6 과 같은 선). 어느 외부 시스템의 이름도 이 파일에 나오지 않으며, 그것은
|
|
25
|
+
// core/** 전체에 걸린 기존 규칙이기도 하다.
|
|
26
|
+
import { z } from 'zod';
|
|
27
|
+
import { QUERY_ID } from "../model/ids.js";
|
|
28
|
+
/**
|
|
29
|
+
* 외부 시스템이 준 **안정적인** 신원 (F3).
|
|
30
|
+
*
|
|
31
|
+
* 사람이 보는 링크는 여기 있어도 되지만 그것이 정본은 아니다. 링크의 모양이 예상과
|
|
32
|
+
* 다르다는 이유로 생성이 실패했다고 읽고 하나 더 만든 사고가 실제로 있었다 — 그때
|
|
33
|
+
* 대조할 수 있는 다른 값이 계약에 없었다.
|
|
34
|
+
*/
|
|
35
|
+
export const RemoteIdentity = z.object({
|
|
36
|
+
/** 어느 adapter 가 이 사실을 만들었는가. provider 이름이 아니라 adapter id 다. */
|
|
37
|
+
adapter: z.string().min(1),
|
|
38
|
+
/** 그 adapter 의 어휘로 된 객체 종류. Core 는 이 문자열로 분기하지 않는다. */
|
|
39
|
+
objectType: z.string().min(1),
|
|
40
|
+
/** 그 시스템 안에서 이 객체를 다시 찾을 수 있는 값. **성공의 정본이다.** */
|
|
41
|
+
objectId: z.string().min(1),
|
|
42
|
+
/** 어느 자원 아래인가 (저장소·프로젝트 등). adapter 어휘 그대로. */
|
|
43
|
+
resource: z.string().min(1).optional(),
|
|
44
|
+
/**
|
|
45
|
+
* 사람이 열어 보는 주소. **판정에 쓰지 않는다** — 모양이 바뀌어도 같은 객체다.
|
|
46
|
+
*/
|
|
47
|
+
locator: z.string().min(1).optional(),
|
|
48
|
+
/** 이 시점의 판본. 있으면 응답 중복을 가리는 데 쓴다. */
|
|
49
|
+
revisionMarker: z.string().min(1).optional(),
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* 기대가 실제로 밖에 게시됐다는 증거.
|
|
53
|
+
*
|
|
54
|
+
* **이 레코드를 만드는 것이 게시가 아니다.** 게시는 adapter 가 하고, 여기 남는 것은
|
|
55
|
+
* 그것이 일어났다는 관측이다 — 그 구분이 사라지면 "적어 뒀으니 됐다"가 다시 성립한다.
|
|
56
|
+
*/
|
|
57
|
+
export const CommunicationEvidence = z.object({
|
|
58
|
+
evidenceId: z.string().min(1),
|
|
59
|
+
/** 어느 기대에 대한 것인가. 지금은 Bounded Query 가 기대의 정본이다 (C-04). */
|
|
60
|
+
queryId: z.string().regex(QUERY_ID),
|
|
61
|
+
/** 어느 binding 을 통해 나갔는가 (C-09). 역할 이름이며 provider 이름이 아니다. */
|
|
62
|
+
bindingRole: z.string().min(1).optional(),
|
|
63
|
+
identity: RemoteIdentity,
|
|
64
|
+
/** 누구에게 닿아야 하는가. 프로젝트 정책이 정하고 여기서는 그대로 보관한다. */
|
|
65
|
+
audience: z.array(z.string()).default([]),
|
|
66
|
+
publishedAt: z.string().min(1),
|
|
67
|
+
/** 이 사실을 우리가 언제 봤는가. 게시 시각과 다를 수 있다. */
|
|
68
|
+
observedAt: z.string().min(1),
|
|
69
|
+
/** 선언인가 관측인가 (C-10 의 같은 필드와 같은 뜻). */
|
|
70
|
+
evidenceSource: z.string().min(1),
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* 그 게시물에 외부 응답이 도착했다는 증거.
|
|
74
|
+
*
|
|
75
|
+
* **답의 의미를 판정하지 않는다.** 이것은 `QueryAnswer` 가 아니고, 자동으로 그것이
|
|
76
|
+
* 되지도 않는다 — 누가 무엇을 결정할 수 있는지는 C-04 의 권한 판정이고, 사람 승인은
|
|
77
|
+
* C-13 이다. 여기 있는 것은 "왔다"까지다.
|
|
78
|
+
*/
|
|
79
|
+
export const ResponseEvidence = z.object({
|
|
80
|
+
evidenceId: z.string().min(1),
|
|
81
|
+
/** 어느 게시물에 대한 응답인가. */
|
|
82
|
+
communicationId: z.string().min(1),
|
|
83
|
+
identity: RemoteIdentity,
|
|
84
|
+
/** 그 시스템이 말하는 응답자. 우리 역할 이름으로 번역하지 않는다. */
|
|
85
|
+
responder: z.string().min(1).optional(),
|
|
86
|
+
receivedAt: z.string().min(1),
|
|
87
|
+
observedAt: z.string().min(1),
|
|
88
|
+
evidenceSource: z.string().min(1),
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* 기대 하나의 지금 상태를 계산한다.
|
|
92
|
+
*
|
|
93
|
+
* 순서가 곧 규칙이다: 응답이 있으면 왔다, 없으면 기다리는지 아닌지, 게시가 없으면
|
|
94
|
+
* 아직 나가지 않았다. **없는 것을 있는 것으로 올리는 방향은 없다.**
|
|
95
|
+
*/
|
|
96
|
+
export function deriveCoordination(input) {
|
|
97
|
+
const communications = [...input.communications].sort((a, b) => a.evidenceId.localeCompare(b.evidenceId));
|
|
98
|
+
const responses = [...input.responses].sort((a, b) => a.evidenceId.localeCompare(b.evidenceId));
|
|
99
|
+
const state = communications.length === 0
|
|
100
|
+
? 'UNPUBLISHED'
|
|
101
|
+
: responses.length > 0
|
|
102
|
+
? 'RESPONSE_RECEIVED'
|
|
103
|
+
: input.expectsResponse
|
|
104
|
+
? 'WAITING_EXTERNAL'
|
|
105
|
+
: 'PUBLISHED';
|
|
106
|
+
return { queryId: input.queryId, state, expectsResponse: input.expectsResponse, communications, responses };
|
|
107
|
+
}
|
|
108
|
+
const communicationKey = (id) => `coord:pub:${id}`;
|
|
109
|
+
const responseKey = (id) => `coord:res:${id}`;
|
|
110
|
+
/**
|
|
111
|
+
* 게시물에 붙은 글 중 **응답으로 셀 것**을 고른다.
|
|
112
|
+
*
|
|
113
|
+
* 두 가지를 뺀다: 그 시스템이 스스로 남긴 자국(커밋 언급·배정 변경)과, 우리가 쓴 글.
|
|
114
|
+
* 둘 다 세면 아무도 답하지 않은 스레드가 답이 온 것으로 보인다 — 이 구조가 막으려는
|
|
115
|
+
* 바로 그 착각이다.
|
|
116
|
+
*
|
|
117
|
+
* **답의 의미는 판정하지 않는다.** 여기서 나오는 것은 "왔다"까지이고, 그것이 결정인지
|
|
118
|
+
* 승인인지는 C-04·C-13 의 몫이다.
|
|
119
|
+
*/
|
|
120
|
+
export function responsesFrom(communication, remarks, mine) {
|
|
121
|
+
return remarks
|
|
122
|
+
.filter((remark) => !remark.system && !mine.has(remark.author))
|
|
123
|
+
.map((remark) => ({
|
|
124
|
+
// 같은 글을 다시 봐도 같은 id 다 — append-only 원장이 두 번째를 거절한다.
|
|
125
|
+
evidenceId: `${communication.evidenceId}:${remark.id}`,
|
|
126
|
+
communicationId: communication.evidenceId,
|
|
127
|
+
identity: { ...communication.identity, revisionMarker: remark.at },
|
|
128
|
+
responder: remark.author,
|
|
129
|
+
receivedAt: remark.at,
|
|
130
|
+
evidenceSource: 'surface-remark',
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
export class CoordinationLedger {
|
|
134
|
+
#scope;
|
|
135
|
+
#now;
|
|
136
|
+
constructor(scope, now = () => new Date().toISOString()) {
|
|
137
|
+
this.#scope = scope;
|
|
138
|
+
this.#now = now;
|
|
139
|
+
}
|
|
140
|
+
async publishRecorded(input) {
|
|
141
|
+
const evidence = CommunicationEvidence.parse({ ...input, observedAt: input.observedAt ?? this.#now() });
|
|
142
|
+
const written = await this.#scope.setIfAbsent(communicationKey(evidence.evidenceId), JSON.stringify(evidence));
|
|
143
|
+
return written
|
|
144
|
+
? { ok: true, evidence }
|
|
145
|
+
: { ok: false, reason: 'ALREADY_EXISTS', detail: `${evidence.evidenceId} 는 이미 있다` };
|
|
146
|
+
}
|
|
147
|
+
async responseRecorded(input) {
|
|
148
|
+
const target = await this.#scope.get(communicationKey(input.communicationId));
|
|
149
|
+
if (!target) {
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
reason: 'COMMUNICATION_NOT_FOUND',
|
|
153
|
+
detail: `${input.communicationId} 를 찾지 못했다 — 게시된 적 없는 것에 응답을 붙이지 않는다`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const evidence = ResponseEvidence.parse({ ...input, observedAt: input.observedAt ?? this.#now() });
|
|
157
|
+
const written = await this.#scope.setIfAbsent(responseKey(evidence.evidenceId), JSON.stringify(evidence));
|
|
158
|
+
return written
|
|
159
|
+
? { ok: true, evidence }
|
|
160
|
+
: { ok: false, reason: 'ALREADY_EXISTS', detail: `${evidence.evidenceId} 는 이미 있다` };
|
|
161
|
+
}
|
|
162
|
+
async communications() {
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const key of await this.#scope.keys('coord:pub:')) {
|
|
165
|
+
const raw = await this.#scope.get(key);
|
|
166
|
+
if (raw)
|
|
167
|
+
out.push(CommunicationEvidence.parse(JSON.parse(raw)));
|
|
168
|
+
}
|
|
169
|
+
return out.sort((a, b) => a.evidenceId.localeCompare(b.evidenceId));
|
|
170
|
+
}
|
|
171
|
+
async responses() {
|
|
172
|
+
const out = [];
|
|
173
|
+
for (const key of await this.#scope.keys('coord:res:')) {
|
|
174
|
+
const raw = await this.#scope.get(key);
|
|
175
|
+
if (raw)
|
|
176
|
+
out.push(ResponseEvidence.parse(JSON.parse(raw)));
|
|
177
|
+
}
|
|
178
|
+
return out.sort((a, b) => a.evidenceId.localeCompare(b.evidenceId));
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 이미 이 게시물이 있는가 — **원격 신원으로** 찾는다 (F3).
|
|
182
|
+
*
|
|
183
|
+
* 같은 것을 두 번 만들지 않기 위한 조회다. 제목이나 링크가 아니라 adapter 가 준
|
|
184
|
+
* 안정 id 로 본다.
|
|
185
|
+
*/
|
|
186
|
+
async findByIdentity(identity) {
|
|
187
|
+
for (const evidence of await this.communications()) {
|
|
188
|
+
const seen = evidence.identity;
|
|
189
|
+
if (seen.adapter === identity.adapter && seen.objectType === identity.objectType && seen.objectId === identity.objectId) {
|
|
190
|
+
return evidence;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
/** 기대 하나에 달린 증거 전부. */
|
|
196
|
+
async forQuery(queryId) {
|
|
197
|
+
const communications = (await this.communications()).filter((evidence) => evidence.queryId === queryId);
|
|
198
|
+
const ids = new Set(communications.map((evidence) => evidence.evidenceId));
|
|
199
|
+
const responses = (await this.responses()).filter((evidence) => ids.has(evidence.communicationId));
|
|
200
|
+
return { communications, responses };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 기대 목록과 증거를 합쳐 지금 상태를 낸다.
|
|
205
|
+
*
|
|
206
|
+
* 기대는 이 모듈이 만들지 않는다 — Bounded Query 가 정본이고, 여기서는 그것을 받아
|
|
207
|
+
* 증거와 맞춘다. 기대가 없으면 조율도 없다.
|
|
208
|
+
*/
|
|
209
|
+
export async function viewCoordination(ledger, expectations) {
|
|
210
|
+
const views = [];
|
|
211
|
+
for (const expectation of expectations) {
|
|
212
|
+
const { communications, responses } = await ledger.forQuery(expectation.id);
|
|
213
|
+
views.push(deriveCoordination({
|
|
214
|
+
queryId: expectation.id,
|
|
215
|
+
expectsResponse: expectation.expectsResponse,
|
|
216
|
+
communications,
|
|
217
|
+
responses,
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
return views.sort((a, b) => a.queryId.localeCompare(b.queryId));
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* 사람이 읽는 줄. **없는 것을 0 으로 그리지 않는다** — 기대가 없으면 그렇게 말한다.
|
|
224
|
+
*/
|
|
225
|
+
export function coordinationLines(views) {
|
|
226
|
+
if (views.length === 0)
|
|
227
|
+
return ['No external expectation is recorded here.'];
|
|
228
|
+
const lines = [];
|
|
229
|
+
const counts = {
|
|
230
|
+
UNPUBLISHED: 0,
|
|
231
|
+
WAITING_EXTERNAL: 0,
|
|
232
|
+
RESPONSE_RECEIVED: 0,
|
|
233
|
+
PUBLISHED: 0,
|
|
234
|
+
};
|
|
235
|
+
for (const view of views)
|
|
236
|
+
counts[view.state] += 1;
|
|
237
|
+
lines.push(`Coordination: ${views.length} expectation${views.length === 1 ? '' : 's'} — ` +
|
|
238
|
+
`unpublished ${counts.UNPUBLISHED} · waiting ${counts.WAITING_EXTERNAL} · ` +
|
|
239
|
+
`answered ${counts.RESPONSE_RECEIVED} · published ${counts.PUBLISHED}`);
|
|
240
|
+
for (const view of views) {
|
|
241
|
+
const where = view.communications[0]?.identity;
|
|
242
|
+
// 링크는 사람이 열어 보는 값이다. 없으면 안정 신원을 보인다 — 판정의 정본은 그쪽이다.
|
|
243
|
+
const at = where ? ` @ ${where.locator ?? `${where.adapter}:${where.objectType}:${where.objectId}`}` : '';
|
|
244
|
+
lines.push(` ${view.queryId} ${view.state}${at}`);
|
|
245
|
+
}
|
|
246
|
+
return lines;
|
|
247
|
+
}
|