@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,88 @@
|
|
|
1
|
+
// Coverage Health Escalation — 못 본 것을 못 봤다고 먼저 말한다 (C-12 §3).
|
|
2
|
+
//
|
|
3
|
+
// Coverage Health 값 자체는 B-31부터 있었다. 문제는 **사람이 `asc monitor status` 를 쳐야만
|
|
4
|
+
// 보인다**는 것이었다. 감시가 상시로 도는데 감시의 고장은 수동 조회로만 알 수 있으면,
|
|
5
|
+
// 가장 조용한 실패 모드가 남는다:
|
|
6
|
+
//
|
|
7
|
+
// 외부 소스가 죽었다 → 사건이 안 온다 → "변경 없음" 처럼 보인다
|
|
8
|
+
//
|
|
9
|
+
// **"변경 없음"과 "못 봄"을 합치지 않는다** (C-12 불변식 ⑫). 이 파일이 하는 일은 그 둘을
|
|
10
|
+
// 가르는 판정 하나뿐이다.
|
|
11
|
+
//
|
|
12
|
+
// 판정은 하되 **승인 요청을 만들지 않는다** (불변식 ⑬). 결과는 사람이 볼 목록이며,
|
|
13
|
+
// 기존 Presentation·Digest 경로로 나간다 — 새 채널도 새 상태도 만들지 않는다.
|
|
14
|
+
//
|
|
15
|
+
// 임계값은 Core 상수가 아니다 (불변식 ⑭). 호출자가 정해 넣는다.
|
|
16
|
+
/**
|
|
17
|
+
* 지금 감시가 어디까지 성립하는가.
|
|
18
|
+
*
|
|
19
|
+
* **추측하지 않는다.** 사건이 안 오는 것이 조용한 것인지 끊긴 것인지 여기서 정하지 않고,
|
|
20
|
+
* "오래 안 왔다"는 사실만 든다 — 사람이 그 둘을 가른다.
|
|
21
|
+
*/
|
|
22
|
+
export function evaluateHealth(health, at, thresholds) {
|
|
23
|
+
const alerts = [];
|
|
24
|
+
const now = new Date(at).getTime();
|
|
25
|
+
const elapsed = (since) => {
|
|
26
|
+
if (!since)
|
|
27
|
+
return undefined;
|
|
28
|
+
const value = now - new Date(since).getTime();
|
|
29
|
+
return Number.isNaN(value) ? undefined : value;
|
|
30
|
+
};
|
|
31
|
+
if (!health.sourceHealthy) {
|
|
32
|
+
alerts.push({
|
|
33
|
+
kind: 'SOURCE_UNHEALTHY',
|
|
34
|
+
detail: health.detail ?? 'the external source could not be read — this is not "no changes", it is "not seen"',
|
|
35
|
+
...(health.lastHotEventAt ? { lastAt: health.lastHotEventAt } : {}),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// 한 번도 안 돈 것과 오래 안 돈 것은 다르다. 전자는 설정 문제이고 후자는 운영 문제다.
|
|
39
|
+
if (!health.lastHotEventAt && !health.lastReconcileAt && !health.lastCensusAt) {
|
|
40
|
+
alerts.push({ kind: 'NEVER_RAN', detail: 'monitoring has never run — it was not started, or nothing triggered it' });
|
|
41
|
+
return alerts;
|
|
42
|
+
}
|
|
43
|
+
const hot = elapsed(health.lastHotEventAt);
|
|
44
|
+
if (hot === undefined || hot >= thresholds.hotPathMs) {
|
|
45
|
+
alerts.push({
|
|
46
|
+
kind: 'HOT_PATH_STALE',
|
|
47
|
+
detail: health.lastHotEventAt === undefined
|
|
48
|
+
? '빠른 경로로 사건이 온 적이 없다 — 조용한 것인지 끊긴 것인지 모른다'
|
|
49
|
+
: `빠른 경로가 조용한 지 ${minutes(hot)}분 — 조용한 것인지 끊긴 것인지 모른다`,
|
|
50
|
+
...(health.lastHotEventAt ? { lastAt: health.lastHotEventAt } : {}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const reconcile = elapsed(health.lastReconcileAt);
|
|
54
|
+
if (reconcile === undefined || reconcile >= thresholds.reconcileMs) {
|
|
55
|
+
alerts.push({
|
|
56
|
+
kind: 'RECONCILE_STALE',
|
|
57
|
+
detail: health.lastReconcileAt === undefined
|
|
58
|
+
? 'the reconcile path has never run — nothing has picked up what the fast path missed'
|
|
59
|
+
: `${minutes(reconcile)} min since the reconcile path last ran`,
|
|
60
|
+
...(health.lastReconcileAt ? { lastAt: health.lastReconcileAt } : {}),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const census = elapsed(health.lastCensusAt);
|
|
64
|
+
if (census === undefined || census >= thresholds.censusMs) {
|
|
65
|
+
alerts.push({
|
|
66
|
+
kind: 'CENSUS_STALE',
|
|
67
|
+
detail: health.lastCensusAt === undefined
|
|
68
|
+
? 'listing integrity has never been checked'
|
|
69
|
+
: `listing integrity last checked ${minutes(census)} min ago`,
|
|
70
|
+
...(health.lastCensusAt ? { lastAt: health.lastCensusAt } : {}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// 완주하지 못한 상태에서는 상실 판정 자체가 보류된다 — 그 사실을 사람이 알아야 한다.
|
|
74
|
+
if (!health.paginationComplete && (health.lastReconcileAt || health.lastCensusAt)) {
|
|
75
|
+
alerts.push({
|
|
76
|
+
kind: 'PAGINATION_INCOMPLETE',
|
|
77
|
+
detail: 'the last listing did not complete — disappearances are not judged in this state',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return alerts;
|
|
81
|
+
}
|
|
82
|
+
const minutes = (ms) => Math.floor(ms / 60_000);
|
|
83
|
+
/** 사람이 읽는 블록. 조용한 실패를 조용하게 두지 않는 것이 목적이다. */
|
|
84
|
+
export function healthAlertLines(alerts) {
|
|
85
|
+
if (alerts.length === 0)
|
|
86
|
+
return [];
|
|
87
|
+
return ['Monitoring warnings:', ...alerts.map((alert) => ` [${alert.kind}] ${alert.detail}`)];
|
|
88
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ChangeContextPort } from '../../ports/change-context.ts';
|
|
2
|
+
import type { HistoryPort, ResourceContextPort } from '../../ports/resource-context.ts';
|
|
3
|
+
import type { CanonicalSnapshot } from '../model/entities.ts';
|
|
4
|
+
import { type OwnershipMap } from '../policy/ownership.ts';
|
|
5
|
+
import type { Relevance } from './relevance.ts';
|
|
6
|
+
export declare const STEPS: readonly ["resource", "delta", "responsibility", "work", "thread", "change", "work-context", "canonical", "relevance", "recommendation", "draft"];
|
|
7
|
+
export type StepId = (typeof STEPS)[number];
|
|
8
|
+
export type StepResult = {
|
|
9
|
+
id: StepId;
|
|
10
|
+
kind: 'DONE';
|
|
11
|
+
findings: string[];
|
|
12
|
+
}
|
|
13
|
+
/** 필요한 Port가 없거나 조회가 실패했다. **통과가 아니다.** */
|
|
14
|
+
| {
|
|
15
|
+
id: StepId;
|
|
16
|
+
kind: 'UNDECIDABLE';
|
|
17
|
+
detail: string;
|
|
18
|
+
}
|
|
19
|
+
/** 이 사건에는 해당하지 않는다 (변경이 없는 사건의 change 단계 등). */
|
|
20
|
+
| {
|
|
21
|
+
id: StepId;
|
|
22
|
+
kind: 'SKIPPED';
|
|
23
|
+
detail: string;
|
|
24
|
+
};
|
|
25
|
+
export type Investigation = {
|
|
26
|
+
steps: StepResult[];
|
|
27
|
+
/** 사람이 읽는 상황. 단계 산출을 이어 붙인 것이며 요약하지 않는다. */
|
|
28
|
+
situation: string[];
|
|
29
|
+
recommendation: string;
|
|
30
|
+
/** 확인하지 못한 것. 비어 있지 않으면 draft를 만들지 않는다. */
|
|
31
|
+
undecidable: string[];
|
|
32
|
+
draft?: string;
|
|
33
|
+
/** draft를 만들지 않은 이유. 만들었으면 없다. */
|
|
34
|
+
draftBlocked?: string;
|
|
35
|
+
};
|
|
36
|
+
export type InvestigationInput = {
|
|
37
|
+
reference: string;
|
|
38
|
+
/** Phase A가 이미 낸 판정. 여기서 다시 계산하지 않는다. */
|
|
39
|
+
relevance?: Relevance;
|
|
40
|
+
/** 지난 관측 — ② delta 단계의 기준. */
|
|
41
|
+
previous?: {
|
|
42
|
+
revisionMarker: string;
|
|
43
|
+
state?: string;
|
|
44
|
+
};
|
|
45
|
+
/** 이 사건에 걸린 결정 영역. 없으면 responsibility 단계는 owner 확인까지다. */
|
|
46
|
+
decisionDomains?: readonly string[];
|
|
47
|
+
ownership?: OwnershipMap;
|
|
48
|
+
owner?: string;
|
|
49
|
+
activeSessions?: readonly string[];
|
|
50
|
+
canonicalPaths?: readonly string[];
|
|
51
|
+
/** 조사 예산. inspect는 최근 것만, trace는 더 넓게 본다 (C-05 §3). */
|
|
52
|
+
commentLimit?: number;
|
|
53
|
+
/**
|
|
54
|
+
* 이 사건에 연결된 작업 항목 (⑦). 코드 쪽 reference와 **다른 Binding일 수 있다** —
|
|
55
|
+
* 코드가 한 곳, 작업 항목이 다른 곳인 것이 정상이다 (C-09 §3.1).
|
|
56
|
+
* 선언되지 않으면 그 단계는 해당 없음이다.
|
|
57
|
+
*/
|
|
58
|
+
workReference?: string;
|
|
59
|
+
/** 경위를 어디까지 볼지. trace가 아니면 굳이 넓히지 않는다. */
|
|
60
|
+
historyLimit?: number;
|
|
61
|
+
};
|
|
62
|
+
export type InvestigationPorts = {
|
|
63
|
+
resource?: ResourceContextPort;
|
|
64
|
+
change?: ChangeContextPort;
|
|
65
|
+
/**
|
|
66
|
+
* 작업 항목 쪽 통로 (⑦). 코드 쪽과 **다른 adapter일 수 있다** — 같은 것을 쓰라고
|
|
67
|
+
* 강요하면 두 시스템을 동시에 붙일 수 없다.
|
|
68
|
+
*/
|
|
69
|
+
work?: ResourceContextPort;
|
|
70
|
+
/** 경위 조회. 제공하지 못하는 adapter가 흔하므로 따로 받는다 (C-09 §2.1). */
|
|
71
|
+
history?: HistoryPort;
|
|
72
|
+
/** 정본 baseline 조회. 없으면 canonical 단계가 성립하지 않는다. */
|
|
73
|
+
baselines?: () => Promise<CanonicalSnapshot[]>;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* 단계를 순서대로 밟는다. **각 단계는 자기가 필요한 Port만 요청한다** — 어떤 외부 시스템이
|
|
77
|
+
* 그것을 제공하는지는 Binding이 정한다 (C-09 §4).
|
|
78
|
+
*
|
|
79
|
+
* 이미 끝난 단계가 있으면 그 자리는 건너뛴다. 재시도가 조사를 처음부터 다시 하면 실패
|
|
80
|
+
* 지점이 비쌀수록 영영 넘지 못한다 (C-07 §6.3).
|
|
81
|
+
*/
|
|
82
|
+
export declare function investigate(input: InvestigationInput, ports: InvestigationPorts, done?: readonly StepResult[]): Promise<Investigation>;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Typed Investigation — "AI가 알아서 확인"을 단계 계약으로 바꾼다 (C-07 §6).
|
|
2
|
+
//
|
|
3
|
+
// 조사를 자유 서술로 두면 무엇을 봤고 무엇을 못 봤는지가 남지 않는다. 그러면 못 본 것이
|
|
4
|
+
// 조용히 "문제 없음"이 되고, 사람은 확인되지 않은 것을 확인된 것으로 읽는다.
|
|
5
|
+
//
|
|
6
|
+
// 단계마다 두 가지가 분명해야 한다:
|
|
7
|
+
// 무엇을 필요로 하는가 — 그 단계가 요청하는 Port. provider 이름으로 갈라지지 않는다
|
|
8
|
+
// 못 했으면 왜 못 했는가 — 판정 불성립은 통과가 아니다. 다른 단계 결과로 대신하지 않는다
|
|
9
|
+
import { isWithinScopes } from "../policy/scope.js";
|
|
10
|
+
import { lookupAuthority } from "../policy/ownership.js";
|
|
11
|
+
export const STEPS = [
|
|
12
|
+
'resource', // ① 사건 자체 재확인 — 요약을 믿지 않는다
|
|
13
|
+
'delta', // ② 직전 관측 대비 무엇이 달라졌나
|
|
14
|
+
'responsibility', // ③ 누가 끌고 가고 누가 결정하는가
|
|
15
|
+
'work', // ④ 지금 돌고 있는 것과의 관계
|
|
16
|
+
'thread', // ⑤ 논의
|
|
17
|
+
'change', // ⑥ 변경 경로·요약
|
|
18
|
+
'work-context', // ⑦ 작업 항목의 상태·연결 (다른 Binding일 수 있다)
|
|
19
|
+
'canonical', // ⑧ 정본 대조
|
|
20
|
+
'relevance', // ⑨ 관련성과 영향
|
|
21
|
+
'recommendation', // ⑩ 무엇을 하면 되는가
|
|
22
|
+
'draft', // ⑪ 초안 (조건을 만족할 때만)
|
|
23
|
+
];
|
|
24
|
+
const missingPort = (id, what) => ({
|
|
25
|
+
id,
|
|
26
|
+
kind: 'UNDECIDABLE',
|
|
27
|
+
detail: `${what} 를 제공하는 binding이 없다 — 이 단계는 확인하지 못했다`,
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* 단계를 순서대로 밟는다. **각 단계는 자기가 필요한 Port만 요청한다** — 어떤 외부 시스템이
|
|
31
|
+
* 그것을 제공하는지는 Binding이 정한다 (C-09 §4).
|
|
32
|
+
*
|
|
33
|
+
* 이미 끝난 단계가 있으면 그 자리는 건너뛴다. 재시도가 조사를 처음부터 다시 하면 실패
|
|
34
|
+
* 지점이 비쌀수록 영영 넘지 못한다 (C-07 §6.3).
|
|
35
|
+
*/
|
|
36
|
+
export async function investigate(input, ports, done = []) {
|
|
37
|
+
const steps = [...done];
|
|
38
|
+
const already = new Set(done.map((s) => s.id));
|
|
39
|
+
const put = (result) => {
|
|
40
|
+
if (!already.has(result.id))
|
|
41
|
+
steps.push(result);
|
|
42
|
+
};
|
|
43
|
+
// ① 사건 자체 — 알림 요약을 그대로 믿지 않는다.
|
|
44
|
+
const resource = ports.resource ? await ports.resource.getResource(input.reference).catch(() => null) : null;
|
|
45
|
+
if (!ports.resource)
|
|
46
|
+
put(missingPort('resource', '리소스 조회'));
|
|
47
|
+
else if (!resource || resource.missing) {
|
|
48
|
+
put({ id: 'resource', kind: 'UNDECIDABLE', detail: '리소스를 읽지 못했다 (사라졌거나 접근 불가)' });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
put({
|
|
52
|
+
id: 'resource',
|
|
53
|
+
kind: 'DONE',
|
|
54
|
+
findings: [
|
|
55
|
+
`${resource.title} — ${resource.state}`,
|
|
56
|
+
...(resource.assignees?.length ? [`배정: ${resource.assignees.join(', ')}`] : []),
|
|
57
|
+
...(resource.labels?.length ? [`라벨: ${resource.labels.join(', ')}`] : []),
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// ② Delta — 현재 모습만 보면 무엇이 새로운지 알 수 없다.
|
|
62
|
+
if (!input.previous) {
|
|
63
|
+
put({ id: 'delta', kind: 'SKIPPED', detail: '지난 관측이 없다 — 처음 보는 사건이다' });
|
|
64
|
+
}
|
|
65
|
+
else if (!resource) {
|
|
66
|
+
put(missingPort('delta', '리소스 조회'));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const changes = [];
|
|
70
|
+
if (input.previous.revisionMarker !== resource.revisionMarker)
|
|
71
|
+
changes.push('실질 변화 있음');
|
|
72
|
+
if (input.previous.state && input.previous.state !== resource.state) {
|
|
73
|
+
changes.push(`상태 ${input.previous.state} → ${resource.state}`);
|
|
74
|
+
}
|
|
75
|
+
put({ id: 'delta', kind: 'DONE', findings: changes.length > 0 ? changes : ['지난 관측과 같다'] });
|
|
76
|
+
}
|
|
77
|
+
// ③ Responsibility — B-23~B-25를 실제로 쓴다 (C-04).
|
|
78
|
+
const responsibility = [];
|
|
79
|
+
if (input.owner)
|
|
80
|
+
responsibility.push(`Owner: ${input.owner}`);
|
|
81
|
+
for (const domain of input.decisionDomains ?? []) {
|
|
82
|
+
const found = lookupAuthority(input.ownership, domain);
|
|
83
|
+
responsibility.push(found.kind === 'RESOLVED'
|
|
84
|
+
? `${domain} → ${found.role}`
|
|
85
|
+
: found.kind === 'AMBIGUOUS'
|
|
86
|
+
? `${domain} → 갈림 (${found.candidates.join(', ')})`
|
|
87
|
+
: `${domain} → 결정권자 미선언`);
|
|
88
|
+
}
|
|
89
|
+
put(responsibility.length > 0
|
|
90
|
+
? { id: 'responsibility', kind: 'DONE', findings: responsibility }
|
|
91
|
+
: { id: 'responsibility', kind: 'SKIPPED', detail: 'owner·결정 영역이 선언되지 않았다' });
|
|
92
|
+
// ④ 지금 돌고 있는 것과의 관계.
|
|
93
|
+
put(input.activeSessions?.length
|
|
94
|
+
? { id: 'work', kind: 'DONE', findings: [`활성 세션: ${input.activeSessions.join(', ')}`] }
|
|
95
|
+
: { id: 'work', kind: 'SKIPPED', detail: '지금 돌고 있는 세션이 없다' });
|
|
96
|
+
// ⑤ Thread — 전부 읽지 않는다. 판단에 필요한 만큼만 (C-05 §3).
|
|
97
|
+
if (!ports.resource)
|
|
98
|
+
put(missingPort('thread', '스레드 조회'));
|
|
99
|
+
else {
|
|
100
|
+
const comments = await ports.resource
|
|
101
|
+
.getComments(input.reference, { limit: input.commentLimit ?? 5 })
|
|
102
|
+
.catch(() => null);
|
|
103
|
+
put(comments === null
|
|
104
|
+
? { id: 'thread', kind: 'UNDECIDABLE', detail: '논의를 읽지 못했다' }
|
|
105
|
+
: {
|
|
106
|
+
id: 'thread',
|
|
107
|
+
kind: 'DONE',
|
|
108
|
+
findings: comments.slice(0, 3).map((c) => `${c.author}: ${c.body.slice(0, 80)}`),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// ⑥ Change — 실제 변경 내용으로 본다. reviewer 지정 여부가 아니다.
|
|
112
|
+
const change = ports.change ? await ports.change.getChange(input.reference).catch(() => null) : null;
|
|
113
|
+
if (!ports.change)
|
|
114
|
+
put(missingPort('change', '변경 조회'));
|
|
115
|
+
else if (!change || change.missing) {
|
|
116
|
+
put({ id: 'change', kind: 'SKIPPED', detail: '변경이 딸린 사건이 아니거나 읽지 못했다' });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
put({
|
|
120
|
+
id: 'change',
|
|
121
|
+
kind: 'DONE',
|
|
122
|
+
findings: [
|
|
123
|
+
`변경 경로 ${change.changedPaths.length}건${change.truncated ? ' (일부만 받았다)' : ''}`,
|
|
124
|
+
...change.changedPaths.slice(0, 5),
|
|
125
|
+
...(change.reviewState ? [`검토: ${change.reviewState}`] : []),
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// ⑦ Work Context — 작업 항목의 상태·연결. 코드 쪽과 다른 Binding일 수 있다.
|
|
130
|
+
if (!input.workReference) {
|
|
131
|
+
put({ id: 'work-context', kind: 'SKIPPED', detail: '연결된 작업 항목이 선언되지 않았다' });
|
|
132
|
+
}
|
|
133
|
+
else if (!ports.work) {
|
|
134
|
+
put(missingPort('work-context', '작업 항목 조회'));
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const item = await ports.work.getResource(input.workReference).catch(() => null);
|
|
138
|
+
if (!item || item.missing) {
|
|
139
|
+
put({ id: 'work-context', kind: 'UNDECIDABLE', detail: '작업 항목을 읽지 못했다' });
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// 경위는 있으면 더한다. 없다고 이 단계 전체가 무너지지는 않는다 —
|
|
143
|
+
// 이력을 모르는 도구가 흔하고, 그것과 "못 읽었다"는 다르다.
|
|
144
|
+
const history = ports.history
|
|
145
|
+
? await ports.history.getHistory(input.workReference, input.historyLimit ?? 3).catch(() => [])
|
|
146
|
+
: [];
|
|
147
|
+
put({
|
|
148
|
+
id: 'work-context',
|
|
149
|
+
kind: 'DONE',
|
|
150
|
+
findings: [
|
|
151
|
+
`${item.title} — ${item.state}`,
|
|
152
|
+
...(item.assignees?.length ? [`담당: ${item.assignees.join(', ')}`] : []),
|
|
153
|
+
...(item.related?.length ? [`연결: ${item.related.join(', ')}`] : []),
|
|
154
|
+
...history.map((event) => `${event.at} ${event.actor} ${event.kind}`),
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ⑧ Canonical — 계약이 고정한 것과 이번 변경을 견준다.
|
|
160
|
+
let canonicalConflict = false;
|
|
161
|
+
if (!ports.baselines)
|
|
162
|
+
put(missingPort('canonical', '정본 조회'));
|
|
163
|
+
else {
|
|
164
|
+
const snapshots = await ports.baselines().catch(() => null);
|
|
165
|
+
if (!snapshots)
|
|
166
|
+
put({ id: 'canonical', kind: 'UNDECIDABLE', detail: '정본을 읽지 못했다' });
|
|
167
|
+
else {
|
|
168
|
+
const touched = (change?.changedPaths ?? []).filter((path) => input.canonicalPaths?.length && isWithinScopes(path, input.canonicalPaths));
|
|
169
|
+
canonicalConflict = touched.length > 0;
|
|
170
|
+
put({
|
|
171
|
+
id: 'canonical',
|
|
172
|
+
kind: 'DONE',
|
|
173
|
+
findings: [
|
|
174
|
+
`정본 ${snapshots.length}갈래 기준`,
|
|
175
|
+
...(canonicalConflict ? [`contract drift 후보 — 정본 영역 변경: ${touched.join(', ')}`] : []),
|
|
176
|
+
],
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// ⑨ Relevance — Phase A 판정을 근거와 함께 옮긴다. 여기서 다시 계산하지 않는다.
|
|
181
|
+
put(input.relevance
|
|
182
|
+
? {
|
|
183
|
+
id: 'relevance',
|
|
184
|
+
kind: 'DONE',
|
|
185
|
+
findings: input.relevance.evidence.map((e) => `${e.supports ? '+' : '-'} ${e.detail}`),
|
|
186
|
+
}
|
|
187
|
+
: { id: 'relevance', kind: 'SKIPPED', detail: '관련성 판정을 받지 못했다' });
|
|
188
|
+
const undecidable = steps
|
|
189
|
+
.filter((s) => s.kind === 'UNDECIDABLE')
|
|
190
|
+
.map((s) => `${s.id}: ${s.detail}`);
|
|
191
|
+
// ⑩ Recommendation — 못 본 것이 있으면 그것부터 말한다.
|
|
192
|
+
const recommendation = undecidable.length > 0
|
|
193
|
+
? '확인하지 못한 것이 있다. 아래를 먼저 채우고 다시 본다.'
|
|
194
|
+
: canonicalConflict
|
|
195
|
+
? '정본 영역이 바뀌었다. 계약 영향을 확인한 뒤 대응한다.'
|
|
196
|
+
: input.relevance?.actual === 'HIGH'
|
|
197
|
+
? '관련이 확인됐다. 대응 여부를 정한다.'
|
|
198
|
+
: '지금은 행동이 필요해 보이지 않는다.';
|
|
199
|
+
put({ id: 'recommendation', kind: 'DONE', findings: [recommendation] });
|
|
200
|
+
// ⑪ Draft — 조건을 만족할 때만 (C-07 §7).
|
|
201
|
+
const blocked = draftBlocker(input, undecidable, canonicalConflict);
|
|
202
|
+
put(blocked
|
|
203
|
+
? { id: 'draft', kind: 'SKIPPED', detail: blocked }
|
|
204
|
+
: { id: 'draft', kind: 'DONE', findings: ['초안 작성 조건을 만족한다'] });
|
|
205
|
+
return {
|
|
206
|
+
steps,
|
|
207
|
+
situation: steps.flatMap((s) => (s.kind === 'DONE' ? s.findings : [`${s.id}: 확인 못 함 — ${s.detail}`])),
|
|
208
|
+
recommendation,
|
|
209
|
+
undecidable,
|
|
210
|
+
...(blocked ? { draftBlocked: blocked } : {}),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 초안을 만들면 안 되는 이유. 단정적인 초안은 그 자체로 판단을 실어 나르고, 사람은 초안을
|
|
215
|
+
* 검토하는 대신 승인하게 된다 (C-07 §7).
|
|
216
|
+
*/
|
|
217
|
+
function draftBlocker(input, undecidable, canonicalConflict) {
|
|
218
|
+
if (undecidable.length > 0)
|
|
219
|
+
return `확인하지 못한 단계가 있다 (${undecidable.length}건)`;
|
|
220
|
+
if (canonicalConflict)
|
|
221
|
+
return '정본과 충돌 가능성이 있다 — 사람이 먼저 정한다';
|
|
222
|
+
if (!input.relevance)
|
|
223
|
+
return '관련성 판정이 없다';
|
|
224
|
+
if (input.relevance.actual !== 'HIGH')
|
|
225
|
+
return '관련 근거가 약하다';
|
|
226
|
+
for (const domain of input.decisionDomains ?? []) {
|
|
227
|
+
const found = lookupAuthority(input.ownership, domain);
|
|
228
|
+
if (found.kind !== 'RESOLVED')
|
|
229
|
+
return `'${domain}' 의 결정권자가 정해지지 않았다`;
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ScopedStore } from '../../ports/state-store.ts';
|
|
3
|
+
import type { Disposition, Relevance } from './relevance.ts';
|
|
4
|
+
/**
|
|
5
|
+
* 이번에 본 모습. 셋 중 하나라도 달라지면 실질 변화다 (C-07 §4.3).
|
|
6
|
+
*
|
|
7
|
+
* evidence를 넣는 이유: 같은 스레드라도 근거가 달라졌으면 새 사건이다. 변경 경로가 내
|
|
8
|
+
* 영역까지 넓어진 것을 "같은 스레드"라고 묶으면 그게 정확히 놓치는 경우다.
|
|
9
|
+
*/
|
|
10
|
+
export type Fingerprint = {
|
|
11
|
+
revisionMarker: string;
|
|
12
|
+
evidence: readonly string[];
|
|
13
|
+
};
|
|
14
|
+
export declare const Observation: z.ZodObject<{
|
|
15
|
+
reference: z.ZodString;
|
|
16
|
+
revisionMarker: z.ZodString;
|
|
17
|
+
evidence: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
18
|
+
/** 지난번 처분. SHADOW면 숨긴 것이고 INBOX면 올린 것이다. */
|
|
19
|
+
disposition: z.ZodEnum<["INBOX", "SHADOW"]>;
|
|
20
|
+
/** 왜 그렇게 처분했는지. 나중에 사람이 "왜 안 보였나"를 물을 때 답이 된다. */
|
|
21
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
22
|
+
firstSeenAt: z.ZodString;
|
|
23
|
+
lastSeenAt: z.ZodString;
|
|
24
|
+
}, "strip", z.ZodTypeAny, {
|
|
25
|
+
reference: string;
|
|
26
|
+
disposition: "INBOX" | "SHADOW";
|
|
27
|
+
evidence: string[];
|
|
28
|
+
revisionMarker: string;
|
|
29
|
+
firstSeenAt: string;
|
|
30
|
+
lastSeenAt: string;
|
|
31
|
+
reason?: string | undefined;
|
|
32
|
+
}, {
|
|
33
|
+
reference: string;
|
|
34
|
+
disposition: "INBOX" | "SHADOW";
|
|
35
|
+
revisionMarker: string;
|
|
36
|
+
firstSeenAt: string;
|
|
37
|
+
lastSeenAt: string;
|
|
38
|
+
evidence?: string[] | undefined;
|
|
39
|
+
reason?: string | undefined;
|
|
40
|
+
}>;
|
|
41
|
+
export type Observation = z.infer<typeof Observation>;
|
|
42
|
+
export type SurfaceDecision = {
|
|
43
|
+
surface: true;
|
|
44
|
+
reason: 'NEW' | 'MATERIAL_CHANGE' | 'PROMOTED';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 올리지 않는다. 두 이유를 나누는 까닭: `SHADOWED`는 관련 근거가 없어서이고
|
|
48
|
+
* `NO_MATERIAL_CHANGE`는 관련은 있는데 지난번과 같아서다. 사람이 "왜 안 보였나"를
|
|
49
|
+
* 물을 때 답이 다르다.
|
|
50
|
+
*/
|
|
51
|
+
| {
|
|
52
|
+
surface: false;
|
|
53
|
+
reason: 'SHADOWED' | 'NO_MATERIAL_CHANGE';
|
|
54
|
+
previous?: Observation;
|
|
55
|
+
};
|
|
56
|
+
/** 근거를 비교 가능한 문자열로. 순서가 흔들려 오탐하지 않게 정렬한다. */
|
|
57
|
+
export declare const fingerprintOf: (relevance: Relevance, revisionMarker: string) => Fingerprint;
|
|
58
|
+
export declare class ObservationLedger {
|
|
59
|
+
#private;
|
|
60
|
+
constructor(scope: ScopedStore, now?: () => string);
|
|
61
|
+
/**
|
|
62
|
+
* 이번 관측을 올릴 것인가.
|
|
63
|
+
*
|
|
64
|
+
* 처음 보는 것은 올린다. 지난번에 Shadow로 내렸던 것이 이번에 INBOX 판정이면 **승격**이다 —
|
|
65
|
+
* 그게 Shadow를 버리지 않고 두는 이유다.
|
|
66
|
+
*/
|
|
67
|
+
decide(reference: string, fingerprint: Fingerprint, disposition: Disposition): Promise<SurfaceDecision>;
|
|
68
|
+
/** 이번 관측을 기록한다. 처음 본 시각은 보존한다 — 언제부터 지켜봤는지가 사라지면 안 된다. */
|
|
69
|
+
record(reference: string, fingerprint: Fingerprint, disposition: Disposition, reason?: string): Promise<Observation>;
|
|
70
|
+
get(reference: string): Promise<Observation | null>;
|
|
71
|
+
list(): Promise<Observation[]>;
|
|
72
|
+
/**
|
|
73
|
+
* 지금 숨겨 두고 보는 것들. **숨김이지 폐기가 아니다** — 사람이 물으면 그대로 보여준다
|
|
74
|
+
* (C-07 §5.4).
|
|
75
|
+
*/
|
|
76
|
+
shadowed(): Promise<Observation[]>;
|
|
77
|
+
}
|
|
78
|
+
/** 사람이 읽는 줄. 왜 숨겼는지를 함께 적는다 — 이유 없는 숨김은 폐기와 구분되지 않는다. */
|
|
79
|
+
export declare function shadowLines(records: readonly Observation[]): string[];
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Observation Ledger — reference 하나를 계속 지켜본 기록 (C-07 §4·§5).
|
|
2
|
+
//
|
|
3
|
+
// 두 가지를 같은 자리에서 푼다. 둘 다 "이 reference를 지난번에 어떻게 봤나"가 있어야
|
|
4
|
+
// 성립하기 때문이다:
|
|
5
|
+
//
|
|
6
|
+
// Material Change 같은 스레드에서 실질 변화 없이 다시 부를 때 패킷을 만들지 않는다
|
|
7
|
+
// Shadow Watch 지금은 내 일이 아니라고 본 것을 버리지 않고 계속 본다
|
|
8
|
+
//
|
|
9
|
+
// 왜 Request 상태가 아닌가: 상태를 하나 더 만들면 승인 lifecycle 전체가 영향을 받는다
|
|
10
|
+
// (OM §11.2 열거는 동결이다). 그리고 이건 요청에 대한 사실이 아니라 **외부 리소스에 대한
|
|
11
|
+
// 우리 관측**이다 — 요청이 만들어지기 전에도, 만들어지지 않아도 존재한다.
|
|
12
|
+
//
|
|
13
|
+
// 저장은 adapter-scope다 (C-07 §0.2, Closure Ledger·Bounded Query 선례).
|
|
14
|
+
//
|
|
15
|
+
// 여기는 closure.ts와 달리 읽고-고쳐-쓴다. 기록이 갱신돼야 하는 물건이라 setIfAbsent로는
|
|
16
|
+
// 표현되지 않기 때문이다. 안전한 이유는 Monitor Run이 프로젝트 단위 lease로 직렬화되어
|
|
17
|
+
// 같은 scope를 두 Run이 동시에 만지지 않기 때문이다 (engine.ts scan lease).
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
export const Observation = z.object({
|
|
20
|
+
reference: z.string().min(1),
|
|
21
|
+
revisionMarker: z.string(),
|
|
22
|
+
evidence: z.array(z.string()).default([]),
|
|
23
|
+
/** 지난번 처분. SHADOW면 숨긴 것이고 INBOX면 올린 것이다. */
|
|
24
|
+
disposition: z.enum(['INBOX', 'SHADOW']),
|
|
25
|
+
/** 왜 그렇게 처분했는지. 나중에 사람이 "왜 안 보였나"를 물을 때 답이 된다. */
|
|
26
|
+
reason: z.string().optional(),
|
|
27
|
+
firstSeenAt: z.string().min(1),
|
|
28
|
+
lastSeenAt: z.string().min(1),
|
|
29
|
+
});
|
|
30
|
+
const key = (reference) => `obs:${reference}`;
|
|
31
|
+
/** 근거를 비교 가능한 문자열로. 순서가 흔들려 오탐하지 않게 정렬한다. */
|
|
32
|
+
export const fingerprintOf = (relevance, revisionMarker) => ({
|
|
33
|
+
revisionMarker,
|
|
34
|
+
evidence: relevance.evidence
|
|
35
|
+
.filter((e) => e.supports)
|
|
36
|
+
.map((e) => `${e.kind}:${e.detail}`)
|
|
37
|
+
.sort(),
|
|
38
|
+
});
|
|
39
|
+
export class ObservationLedger {
|
|
40
|
+
#scope;
|
|
41
|
+
#now;
|
|
42
|
+
constructor(scope, now = () => new Date().toISOString()) {
|
|
43
|
+
this.#scope = scope;
|
|
44
|
+
this.#now = now;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 이번 관측을 올릴 것인가.
|
|
48
|
+
*
|
|
49
|
+
* 처음 보는 것은 올린다. 지난번에 Shadow로 내렸던 것이 이번에 INBOX 판정이면 **승격**이다 —
|
|
50
|
+
* 그게 Shadow를 버리지 않고 두는 이유다.
|
|
51
|
+
*/
|
|
52
|
+
async decide(reference, fingerprint, disposition) {
|
|
53
|
+
const previous = await this.get(reference);
|
|
54
|
+
if (!previous) {
|
|
55
|
+
return disposition === 'INBOX'
|
|
56
|
+
? { surface: true, reason: 'NEW' }
|
|
57
|
+
: { surface: false, reason: 'SHADOWED' };
|
|
58
|
+
}
|
|
59
|
+
if (previous.disposition === 'SHADOW' && disposition === 'INBOX') {
|
|
60
|
+
return { surface: true, reason: 'PROMOTED' };
|
|
61
|
+
}
|
|
62
|
+
if (disposition === 'SHADOW')
|
|
63
|
+
return { surface: false, reason: 'SHADOWED', previous };
|
|
64
|
+
const changed = previous.revisionMarker !== fingerprint.revisionMarker ||
|
|
65
|
+
previous.evidence.join(' ') !== fingerprint.evidence.join(' ');
|
|
66
|
+
return changed
|
|
67
|
+
? { surface: true, reason: 'MATERIAL_CHANGE' }
|
|
68
|
+
: { surface: false, reason: 'NO_MATERIAL_CHANGE', previous };
|
|
69
|
+
}
|
|
70
|
+
/** 이번 관측을 기록한다. 처음 본 시각은 보존한다 — 언제부터 지켜봤는지가 사라지면 안 된다. */
|
|
71
|
+
async record(reference, fingerprint, disposition, reason) {
|
|
72
|
+
const previous = await this.get(reference);
|
|
73
|
+
const at = this.#now();
|
|
74
|
+
const observation = Observation.parse({
|
|
75
|
+
reference,
|
|
76
|
+
revisionMarker: fingerprint.revisionMarker,
|
|
77
|
+
evidence: [...fingerprint.evidence],
|
|
78
|
+
disposition,
|
|
79
|
+
...(reason ? { reason } : {}),
|
|
80
|
+
firstSeenAt: previous?.firstSeenAt ?? at,
|
|
81
|
+
lastSeenAt: at,
|
|
82
|
+
});
|
|
83
|
+
await this.#scope.set(key(reference), JSON.stringify(observation));
|
|
84
|
+
return observation;
|
|
85
|
+
}
|
|
86
|
+
async get(reference) {
|
|
87
|
+
const raw = await this.#scope.get(key(reference));
|
|
88
|
+
return raw ? Observation.parse(JSON.parse(raw)) : null;
|
|
89
|
+
}
|
|
90
|
+
async list() {
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const stored of await this.#scope.keys('obs:')) {
|
|
93
|
+
const raw = await this.#scope.get(stored);
|
|
94
|
+
if (raw)
|
|
95
|
+
out.push(Observation.parse(JSON.parse(raw)));
|
|
96
|
+
}
|
|
97
|
+
return out.sort((a, b) => a.reference.localeCompare(b.reference));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 지금 숨겨 두고 보는 것들. **숨김이지 폐기가 아니다** — 사람이 물으면 그대로 보여준다
|
|
101
|
+
* (C-07 §5.4).
|
|
102
|
+
*/
|
|
103
|
+
async shadowed() {
|
|
104
|
+
return (await this.list()).filter((o) => o.disposition === 'SHADOW');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** 사람이 읽는 줄. 왜 숨겼는지를 함께 적는다 — 이유 없는 숨김은 폐기와 구분되지 않는다. */
|
|
108
|
+
export function shadowLines(records) {
|
|
109
|
+
return records.map((o) => `${o.reference} — ${o.reason ?? '관련 근거 없음'} (처음 ${o.firstSeenAt} · 마지막 ${o.lastSeenAt})`);
|
|
110
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { OwnershipMap } from '../policy/ownership.ts';
|
|
2
|
+
import type { GenericSignal } from './signals.ts';
|
|
3
|
+
export type RelevanceLevel = 'HIGH' | 'LOW';
|
|
4
|
+
/** 근거의 갈래. 우선순위 순서이며, 마지막(semantic)은 보조다 (C-07 §3.2). */
|
|
5
|
+
export type EvidenceKind = 'ownership' | 'work' | 'contract' | 'participation' | 'semantic' | 'targeting';
|
|
6
|
+
export type RelevanceEvidence = {
|
|
7
|
+
kind: EvidenceKind;
|
|
8
|
+
/** 사람이 읽는 한 줄. 왜 그렇게 봤는지가 여기 남는다. */
|
|
9
|
+
detail: string;
|
|
10
|
+
/** 관련성을 올리는 근거인가, 내리는 근거인가. 반대 근거도 적는다. */
|
|
11
|
+
supports: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type Disposition = 'INBOX' | 'SHADOW';
|
|
14
|
+
export type Relevance = {
|
|
15
|
+
explicit: RelevanceLevel;
|
|
16
|
+
actual: RelevanceLevel;
|
|
17
|
+
evidence: RelevanceEvidence[];
|
|
18
|
+
disposition: Disposition;
|
|
19
|
+
};
|
|
20
|
+
export type RelevanceContext = {
|
|
21
|
+
/** Profile이 선언한 책임 지도 (C-04 §6). */
|
|
22
|
+
ownership?: OwnershipMap;
|
|
23
|
+
/** 이 사람이 맡은 역할들. 비어 있으면 ownership 근거는 성립하지 않는다. */
|
|
24
|
+
myRoles?: readonly string[];
|
|
25
|
+
/** 이번 변경이 건드린 실제 경로. 모르면 생략한다 — 빈 배열과 다르다. */
|
|
26
|
+
changedPaths?: readonly string[];
|
|
27
|
+
/** 지금 돌고 있는 세션들의 쓰기 범위. */
|
|
28
|
+
activeBoundaries?: readonly {
|
|
29
|
+
sessionId: string;
|
|
30
|
+
paths: readonly string[];
|
|
31
|
+
}[];
|
|
32
|
+
/** 정본이 사는 경로. contract 근거의 기준이다. */
|
|
33
|
+
canonicalPaths?: readonly string[];
|
|
34
|
+
/** 내가 전에 이 스레드에 참여했는가. */
|
|
35
|
+
participated?: boolean;
|
|
36
|
+
/** 제목·본문이 내 영역과 연관된다고 볼 근거(보조). Surface가 판단해 넘긴다. */
|
|
37
|
+
semanticHint?: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* 두 축을 판정한다.
|
|
41
|
+
*
|
|
42
|
+
* **숫자를 내지 않는다** (C-07 §3.3). 0.82 같은 값은 사람이 검증할 수 없고, 틀렸을 때 어디가
|
|
43
|
+
* 틀렸는지도 말해주지 않는다. 대신 근거 문장을 남겨 사람이 뒤집을 수 있게 한다.
|
|
44
|
+
*/
|
|
45
|
+
export declare function evaluateRelevance(signals: readonly GenericSignal[], context?: RelevanceContext): Relevance;
|
|
46
|
+
/** 사람이 읽는 근거 블록. 근거 없이 결론만 보여주지 않는다. */
|
|
47
|
+
export declare function renderRelevance(relevance: Relevance): string[];
|