@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,121 @@
|
|
|
1
|
+
import { OperationalPreset, ProfileLock, ProjectProfile, UserOverride } from '../../schemas/profile.ts';
|
|
2
|
+
import type { MonitorConfig } from '../monitor/signals.ts';
|
|
3
|
+
import type { OwnershipMap } from '../policy/ownership.ts';
|
|
4
|
+
import { type ConfigLayer, type ResolvedProfile } from './resolve.ts';
|
|
5
|
+
export type LoadedLayers = {
|
|
6
|
+
profile: ProjectProfile;
|
|
7
|
+
profileSource: string;
|
|
8
|
+
preset?: OperationalPreset;
|
|
9
|
+
presetSource?: string;
|
|
10
|
+
override?: UserOverride;
|
|
11
|
+
};
|
|
12
|
+
export type ResolvedRuntime = {
|
|
13
|
+
resolved: ResolvedProfile;
|
|
14
|
+
layers: LoadedLayers;
|
|
15
|
+
/** Monitor에 그대로 넘길 수 있는 형태로 합쳐 둔다. */
|
|
16
|
+
monitor: MonitorConfig;
|
|
17
|
+
canonicalSources: string[];
|
|
18
|
+
/** 승인 권한자 → 채널:actor. Override가 채운다 (OM §11.6). */
|
|
19
|
+
controllerIdentities: Record<string, string[]>;
|
|
20
|
+
/** 역할 → 쓰기 영역·결정권. Profile만 선언한다 (C-04 §6). 없는 것이 정상이다. */
|
|
21
|
+
ownership?: OwnershipMap;
|
|
22
|
+
degraded: string[];
|
|
23
|
+
};
|
|
24
|
+
export declare const digest: (value: unknown) => string;
|
|
25
|
+
/**
|
|
26
|
+
* 설치 경로에서 Profile과 Preset을, 프로젝트에서 Override를 읽는다.
|
|
27
|
+
* Override가 없는 것은 정상이다 — 팀 설정만으로도 돌아가야 한다.
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadLayers(input: {
|
|
30
|
+
installRoot: string;
|
|
31
|
+
profileId: string;
|
|
32
|
+
presetId?: string;
|
|
33
|
+
overridePath?: string;
|
|
34
|
+
}): Promise<LoadedLayers>;
|
|
35
|
+
/** Vanilla Defaults — 어느 프로젝트에서도 풀리지 않는 바닥 (OM §5.2). */
|
|
36
|
+
export declare function vanillaLayer(): ConfigLayer;
|
|
37
|
+
export declare function toLayers(loaded: LoadedLayers): ConfigLayer[];
|
|
38
|
+
export declare function resolveRuntime(loaded: LoadedLayers, availableCapabilities: readonly string[], ascVersion: string): {
|
|
39
|
+
ok: false;
|
|
40
|
+
failures: import("./resolve.ts").ResolveFailure[];
|
|
41
|
+
} | {
|
|
42
|
+
ok: false;
|
|
43
|
+
failures: {
|
|
44
|
+
kind: "INCOMPATIBLE_CORE";
|
|
45
|
+
detail: string;
|
|
46
|
+
}[];
|
|
47
|
+
runtime?: undefined;
|
|
48
|
+
} | {
|
|
49
|
+
ok: true;
|
|
50
|
+
runtime: ResolvedRuntime;
|
|
51
|
+
failures?: undefined;
|
|
52
|
+
};
|
|
53
|
+
export declare function buildLock(input: {
|
|
54
|
+
runtime: ResolvedRuntime;
|
|
55
|
+
ascVersion: string;
|
|
56
|
+
adapters: Record<string, string>;
|
|
57
|
+
generatedAt: string;
|
|
58
|
+
}): ProfileLock;
|
|
59
|
+
/**
|
|
60
|
+
* 이전 lock을 무손실로 옮긴다 (OM §4.9·§7.4). 덮어쓰기만 하면 어떤 조합으로 돌렸는지가
|
|
61
|
+
* 사라지고, 그러면 "그때는 됐는데"를 재현할 방법이 없다.
|
|
62
|
+
* 파일명에 생성 시각과 digest를 함께 넣어 같은 날 여러 번 바꿔도 부딪히지 않게 한다.
|
|
63
|
+
*/
|
|
64
|
+
export declare function archiveLock(ascRoot: string, previous: ProfileLock): Promise<string>;
|
|
65
|
+
export type LockDrift = {
|
|
66
|
+
field: string;
|
|
67
|
+
locked: string;
|
|
68
|
+
current: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* lock과 지금을 견준다. **자동으로 다시 만들지 않는다** — 무엇이 달라졌는지 사람이 보고
|
|
72
|
+
* 정하는 것이 이 파일의 존재 이유다 (OM §4.9).
|
|
73
|
+
*/
|
|
74
|
+
export declare function compareLock(locked: ProfileLock, current: ProfileLock): LockDrift[];
|
|
75
|
+
export type BootstrapOutcome = {
|
|
76
|
+
ok: true;
|
|
77
|
+
runtime: ResolvedRuntime;
|
|
78
|
+
lock: ProfileLock;
|
|
79
|
+
}
|
|
80
|
+
/** attach 전이다. `.asc/` 자체가 없다 — 설정 없이 도는 경로만 허용된다. */
|
|
81
|
+
| {
|
|
82
|
+
ok: false;
|
|
83
|
+
reason: 'NOT_ATTACHED';
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* `.asc/`는 있는데 lock이 없다. init이 중간에 죽었거나 lock을 잃은 상태이며,
|
|
87
|
+
* attach 전과 구분되지 않으면 무엇으로 도는지 모르는 채 굴러간다.
|
|
88
|
+
*/
|
|
89
|
+
| {
|
|
90
|
+
ok: false;
|
|
91
|
+
reason: 'BROKEN_ATTACHMENT';
|
|
92
|
+
detail: string;
|
|
93
|
+
} | {
|
|
94
|
+
ok: false;
|
|
95
|
+
reason: 'RESOLVE_FAILED';
|
|
96
|
+
details: string[];
|
|
97
|
+
}
|
|
98
|
+
/** 지금 조합이 lock과 다르다. 무엇으로 돌지 사람이 정해야 한다. */
|
|
99
|
+
| {
|
|
100
|
+
ok: false;
|
|
101
|
+
reason: 'LOCK_DRIFT';
|
|
102
|
+
drifts: LockDrift[];
|
|
103
|
+
runtime: ResolvedRuntime;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* 모든 Run이 시작 전에 지나는 문 (OM §4.9). 지금 계층을 다시 합쳐 lock과 견주고,
|
|
107
|
+
* 어긋나면 멈춘다.
|
|
108
|
+
*
|
|
109
|
+
* **자동으로 다시 resolve하거나 lock을 덮지 않는다.** 설정이 바뀐 채로 계속 돌면 그 Run이
|
|
110
|
+
* 무엇을 근거로 판단했는지 나중에 알 수 없고, 사람이 바꾼 것인지 무언가 어긋난 것인지도
|
|
111
|
+
* 구분되지 않는다. 멈추고 묻는 편이 싸다.
|
|
112
|
+
*/
|
|
113
|
+
export declare function bootstrapGuard(input: {
|
|
114
|
+
ascRoot: string;
|
|
115
|
+
installRoot: string;
|
|
116
|
+
profileId?: string;
|
|
117
|
+
presetId?: string;
|
|
118
|
+
capabilities: readonly string[];
|
|
119
|
+
adapters: Record<string, string>;
|
|
120
|
+
ascVersion: string;
|
|
121
|
+
}): Promise<BootstrapOutcome>;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Profile Resolver — 파일에서 계층을 읽어 하나의 Resolved Profile을 만들고,
|
|
2
|
+
// 그 조합을 재현할 수 있게 lock에 박아 둔다 (OM §4.7~4.9).
|
|
3
|
+
//
|
|
4
|
+
// 산출물 셋의 성격이 다르다:
|
|
5
|
+
// resolved-profile — 언제든 다시 만들 수 있는 파생물. 지워도 된다.
|
|
6
|
+
// ASC.md — 사람과 Agent가 읽는 생성물. 정책의 정본이 아니다 (OM §7.1).
|
|
7
|
+
// profile.lock — 재현성 기록. Resolver만 쓰고, 저절로 갱신되지 않는다.
|
|
8
|
+
//
|
|
9
|
+
// 마지막 것이 앞의 둘과 다른 이유: 파생물은 낡으면 다시 만들면 그만이지만, lock이 저 혼자
|
|
10
|
+
// 갱신되면 "무엇으로 돌렸는지"를 영영 잃는다. 그래서 불일치는 고쳐 주지 않고 알린다.
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { constants } from 'node:fs';
|
|
13
|
+
import { access, mkdir, readFile, rename } from 'node:fs/promises';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { OperationalPreset, ProfileLock, ProjectProfile, UserOverride } from "../../schemas/profile.js";
|
|
16
|
+
import { resolveProfile } from "./resolve.js";
|
|
17
|
+
import { satisfies } from "./version.js";
|
|
18
|
+
async function pathExists(path) {
|
|
19
|
+
try {
|
|
20
|
+
await access(path, constants.F_OK);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const digest = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 16);
|
|
28
|
+
async function readJson(path) {
|
|
29
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 설치 경로에서 Profile과 Preset을, 프로젝트에서 Override를 읽는다.
|
|
33
|
+
* Override가 없는 것은 정상이다 — 팀 설정만으로도 돌아가야 한다.
|
|
34
|
+
*/
|
|
35
|
+
export async function loadLayers(input) {
|
|
36
|
+
const profileSource = join(input.installRoot, 'profiles', input.profileId, 'profile.json');
|
|
37
|
+
const profile = ProjectProfile.parse(await readJson(profileSource));
|
|
38
|
+
let preset;
|
|
39
|
+
let presetSource;
|
|
40
|
+
if (input.presetId) {
|
|
41
|
+
presetSource = join(input.installRoot, 'presets', `${input.presetId}.json`);
|
|
42
|
+
preset = OperationalPreset.parse(await readJson(presetSource));
|
|
43
|
+
}
|
|
44
|
+
let override;
|
|
45
|
+
if (input.overridePath) {
|
|
46
|
+
try {
|
|
47
|
+
override = UserOverride.parse(await readJson(input.overridePath));
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code !== 'ENOENT')
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
profile,
|
|
56
|
+
profileSource,
|
|
57
|
+
...(preset ? { preset } : {}),
|
|
58
|
+
...(presetSource ? { presetSource } : {}),
|
|
59
|
+
...(override ? { override } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Vanilla Defaults — 어느 프로젝트에서도 풀리지 않는 바닥 (OM §5.2). */
|
|
63
|
+
export function vanillaLayer() {
|
|
64
|
+
return {
|
|
65
|
+
id: 'vanilla',
|
|
66
|
+
kind: 'vanilla',
|
|
67
|
+
// 외부 write와 정본 수정은 Session 계약으로 열 수 없다. 나가는 길은 Execution Grant뿐이다.
|
|
68
|
+
hardDeny: ['external.write', 'canonical.modify', 'policy.change'],
|
|
69
|
+
softDeny: ['dependency.add', 'shared-module.change'],
|
|
70
|
+
roleScopes: { implementer: ['**'], researcher: ['**'], planner: ['**'], verifier: [] },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function toLayers(loaded) {
|
|
74
|
+
const layers = [vanillaLayer()];
|
|
75
|
+
layers.push({
|
|
76
|
+
id: `profile:${loaded.profile.id}`,
|
|
77
|
+
kind: 'profile',
|
|
78
|
+
...loaded.profile.policy,
|
|
79
|
+
requiredCapabilities: loaded.profile.requires.capabilities,
|
|
80
|
+
optionalCapabilities: loaded.profile.optionalCapabilities,
|
|
81
|
+
});
|
|
82
|
+
if (loaded.preset)
|
|
83
|
+
layers.push({ id: `preset:${loaded.preset.id}`, kind: 'preset', ...loaded.preset.policy });
|
|
84
|
+
if (loaded.override)
|
|
85
|
+
layers.push({ id: 'override:local', kind: 'override', ...loaded.override.policy });
|
|
86
|
+
return layers;
|
|
87
|
+
}
|
|
88
|
+
export function resolveRuntime(loaded, availableCapabilities, ascVersion) {
|
|
89
|
+
const result = resolveProfile(toLayers(loaded), availableCapabilities);
|
|
90
|
+
if (!result.ok)
|
|
91
|
+
return result;
|
|
92
|
+
// Profile이 요구한 Core 범위를 벗어나면 bootstrap 실패다. capability가 모자란 것과
|
|
93
|
+
// 같은 급으로 다룬다 — 맞지 않는 Core에서 도는 것이 없는 기능을 쓰는 것보다 낫지 않다.
|
|
94
|
+
const required = loaded.profile.requires.asc;
|
|
95
|
+
if (required) {
|
|
96
|
+
const check = satisfies(ascVersion, required);
|
|
97
|
+
if (!check.ok) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
failures: [
|
|
101
|
+
{
|
|
102
|
+
kind: 'INCOMPATIBLE_CORE',
|
|
103
|
+
detail: `Profile '${loaded.profile.id}' 는 ASC ${required} 를 요구한다 (${check.reason}: ${check.detail})`,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const monitor = {
|
|
110
|
+
identities: loaded.override?.monitorIdentities ?? [],
|
|
111
|
+
reasonSignals: loaded.profile.monitor.reasonSignals,
|
|
112
|
+
priorityLabels: loaded.profile.monitor.priorityLabels,
|
|
113
|
+
escalationLabels: loaded.profile.monitor.escalationLabels,
|
|
114
|
+
signalPriority: loaded.profile.monitor.signalPriority,
|
|
115
|
+
...(loaded.profile.monitor.inboxSignals ? { inboxSignals: loaded.profile.monitor.inboxSignals } : {}),
|
|
116
|
+
};
|
|
117
|
+
const runtime = {
|
|
118
|
+
resolved: result.profile,
|
|
119
|
+
layers: loaded,
|
|
120
|
+
monitor,
|
|
121
|
+
canonicalSources: loaded.profile.canonical.sources.map((s) => s.id),
|
|
122
|
+
controllerIdentities: loaded.override?.controller.identities ?? {},
|
|
123
|
+
...(loaded.profile.ownership ? { ownership: loaded.profile.ownership } : {}),
|
|
124
|
+
degraded: result.degraded,
|
|
125
|
+
};
|
|
126
|
+
return { ok: true, runtime };
|
|
127
|
+
}
|
|
128
|
+
// ── profile.lock ────────────────────────────────────────────────────────────
|
|
129
|
+
export function buildLock(input) {
|
|
130
|
+
const { layers, resolved } = input.runtime;
|
|
131
|
+
return ProfileLock.parse({
|
|
132
|
+
schemaVersion: 1,
|
|
133
|
+
ascCore: { version: input.ascVersion },
|
|
134
|
+
profile: { id: layers.profile.id, source: layers.profileSource, digest: digest(layers.profile) },
|
|
135
|
+
...(layers.preset && layers.presetSource
|
|
136
|
+
? { preset: { id: layers.preset.id, source: layers.presetSource, digest: digest(layers.preset) } }
|
|
137
|
+
: {}),
|
|
138
|
+
...(layers.override ? { overrideDigest: digest(layers.override) } : {}),
|
|
139
|
+
adapters: input.adapters,
|
|
140
|
+
capabilities: resolved.capabilities,
|
|
141
|
+
// 계층 하나만 달라져도 값이 바뀐다 — 무엇이 달라졌는지는 아래 compareLock이 짚는다
|
|
142
|
+
configurationDigest: digest({
|
|
143
|
+
profile: layers.profile,
|
|
144
|
+
preset: layers.preset ?? null,
|
|
145
|
+
override: layers.override ?? null,
|
|
146
|
+
capabilities: resolved.capabilities,
|
|
147
|
+
adapters: input.adapters,
|
|
148
|
+
}),
|
|
149
|
+
generatedAt: input.generatedAt,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 이전 lock을 무손실로 옮긴다 (OM §4.9·§7.4). 덮어쓰기만 하면 어떤 조합으로 돌렸는지가
|
|
154
|
+
* 사라지고, 그러면 "그때는 됐는데"를 재현할 방법이 없다.
|
|
155
|
+
* 파일명에 생성 시각과 digest를 함께 넣어 같은 날 여러 번 바꿔도 부딪히지 않게 한다.
|
|
156
|
+
*/
|
|
157
|
+
export async function archiveLock(ascRoot, previous) {
|
|
158
|
+
const dir = join(ascRoot, 'archive', 'profile-locks');
|
|
159
|
+
await mkdir(dir, { recursive: true });
|
|
160
|
+
const stamp = previous.generatedAt.replace(/[:.]/g, '-');
|
|
161
|
+
const destination = join(dir, `${stamp}-${previous.configurationDigest}.json`);
|
|
162
|
+
await rename(join(ascRoot, 'profile.lock'), destination);
|
|
163
|
+
return destination;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* lock과 지금을 견준다. **자동으로 다시 만들지 않는다** — 무엇이 달라졌는지 사람이 보고
|
|
167
|
+
* 정하는 것이 이 파일의 존재 이유다 (OM §4.9).
|
|
168
|
+
*/
|
|
169
|
+
export function compareLock(locked, current) {
|
|
170
|
+
const drifts = [];
|
|
171
|
+
const check = (field, a, b) => {
|
|
172
|
+
if ((a ?? '(없음)') !== (b ?? '(없음)'))
|
|
173
|
+
drifts.push({ field, locked: a ?? '(없음)', current: b ?? '(없음)' });
|
|
174
|
+
};
|
|
175
|
+
check('ascCore.version', locked.ascCore.version, current.ascCore.version);
|
|
176
|
+
check('profile.id', locked.profile.id, current.profile.id);
|
|
177
|
+
check('profile.digest', locked.profile.digest, current.profile.digest);
|
|
178
|
+
check('preset.id', locked.preset?.id, current.preset?.id);
|
|
179
|
+
check('preset.digest', locked.preset?.digest, current.preset?.digest);
|
|
180
|
+
check('override.digest', locked.overrideDigest, current.overrideDigest);
|
|
181
|
+
check('capabilities', locked.capabilities.join(','), current.capabilities.join(','));
|
|
182
|
+
for (const name of new Set([...Object.keys(locked.adapters), ...Object.keys(current.adapters)])) {
|
|
183
|
+
check(`adapters.${name}`, locked.adapters[name], current.adapters[name]);
|
|
184
|
+
}
|
|
185
|
+
check('configurationDigest', locked.configurationDigest, current.configurationDigest);
|
|
186
|
+
return drifts;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 모든 Run이 시작 전에 지나는 문 (OM §4.9). 지금 계층을 다시 합쳐 lock과 견주고,
|
|
190
|
+
* 어긋나면 멈춘다.
|
|
191
|
+
*
|
|
192
|
+
* **자동으로 다시 resolve하거나 lock을 덮지 않는다.** 설정이 바뀐 채로 계속 돌면 그 Run이
|
|
193
|
+
* 무엇을 근거로 판단했는지 나중에 알 수 없고, 사람이 바꾼 것인지 무언가 어긋난 것인지도
|
|
194
|
+
* 구분되지 않는다. 멈추고 묻는 편이 싸다.
|
|
195
|
+
*/
|
|
196
|
+
export async function bootstrapGuard(input) {
|
|
197
|
+
let lockedRaw;
|
|
198
|
+
try {
|
|
199
|
+
lockedRaw = await readFile(join(input.ascRoot, 'profile.lock'), 'utf8');
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error.code !== 'ENOENT')
|
|
203
|
+
throw error;
|
|
204
|
+
// `.asc/`가 있느냐 없느냐가 갈림길이다. 없으면 아직 붙이지 않은 것이고,
|
|
205
|
+
// 있는데 lock만 없으면 붙이다 만 것이다 — 뒤엣것은 통과시키면 안 된다.
|
|
206
|
+
try {
|
|
207
|
+
await readFile(join(input.ascRoot, 'ASC.md'), 'utf8');
|
|
208
|
+
}
|
|
209
|
+
catch (inner) {
|
|
210
|
+
if (inner.code === 'ENOENT') {
|
|
211
|
+
const attached = await pathExists(input.ascRoot);
|
|
212
|
+
return attached
|
|
213
|
+
? { ok: false, reason: 'BROKEN_ATTACHMENT', detail: 'profile.lock 이 없다' }
|
|
214
|
+
: { ok: false, reason: 'NOT_ATTACHED' };
|
|
215
|
+
}
|
|
216
|
+
throw inner;
|
|
217
|
+
}
|
|
218
|
+
return { ok: false, reason: 'BROKEN_ATTACHMENT', detail: 'ASC.md 는 있는데 profile.lock 이 없다' };
|
|
219
|
+
}
|
|
220
|
+
const locked = ProfileLock.parse(JSON.parse(lockedRaw));
|
|
221
|
+
const layers = await loadLayers({
|
|
222
|
+
installRoot: input.installRoot,
|
|
223
|
+
profileId: input.profileId ?? locked.profile.id,
|
|
224
|
+
...(input.presetId ?? locked.preset?.id ? { presetId: input.presetId ?? locked.preset.id } : {}),
|
|
225
|
+
overridePath: join(input.ascRoot, 'override.json'),
|
|
226
|
+
});
|
|
227
|
+
const resolved = resolveRuntime(layers, input.capabilities, input.ascVersion);
|
|
228
|
+
if (!resolved.ok) {
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
reason: 'RESOLVE_FAILED',
|
|
232
|
+
details: resolved.failures.map((f) => f.kind === 'POLICY' ? `${f.violation.kind}: ${f.violation.detail}` : `${f.kind}: ${f.detail}`),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const current = buildLock({
|
|
236
|
+
runtime: resolved.runtime,
|
|
237
|
+
ascVersion: input.ascVersion,
|
|
238
|
+
adapters: input.adapters,
|
|
239
|
+
// 시각은 비교 대상이 아니다 — 조합이 같은지만 본다
|
|
240
|
+
generatedAt: locked.generatedAt,
|
|
241
|
+
});
|
|
242
|
+
const drifts = compareLock(locked, current);
|
|
243
|
+
if (drifts.length > 0)
|
|
244
|
+
return { ok: false, reason: 'LOCK_DRIFT', drifts, runtime: resolved.runtime };
|
|
245
|
+
return { ok: true, runtime: resolved.runtime, lock: locked };
|
|
246
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// ASC.md 생성 — Agent가 세션 시작할 때 맨 먼저 읽는 파일 (OM §7.1·§9).
|
|
2
|
+
//
|
|
3
|
+
// 이것은 **생성물**이지 정책의 정본이 아니다. Core Policy와 Profile/Preset/Override를
|
|
4
|
+
// Resolver가 해석한 결과를 사람과 Agent가 읽기 좋게 옮겨 적은 것뿐이며, 직접 고치면
|
|
5
|
+
// 다음 resolve에서 덮인다. 정책을 바꾸려면 입력 계층을 고치고 다시 만든다.
|
|
6
|
+
//
|
|
7
|
+
// controller.md는 여기서 만들지 않는다. 그건 사람이 쓰는 현재 지시이고 Resolver 입력이
|
|
8
|
+
// 아니다 — 둘을 한 파일에 섞으면 "시스템 규칙"과 "오늘의 지시"가 구분되지 않는다.
|
|
9
|
+
import { decisionDomains, lookupAuthority } from "../policy/ownership.js";
|
|
10
|
+
export function renderAscMd(runtime, generatedAt) {
|
|
11
|
+
const { resolved, layers } = runtime;
|
|
12
|
+
const profile = layers.profile;
|
|
13
|
+
const lines = [];
|
|
14
|
+
lines.push('<!-- 생성물입니다. 직접 고치지 마세요 — 다음 resolve에서 덮입니다.', ` 입력: ${resolved.policy.layers.join(' → ')}`, ` 생성: ${generatedAt} -->`, '', `# Agent Session Control — ${profile.id}`, '', '이 파일은 지금 적용되는 운영 규칙이다. 정책을 바꾸려면 이 파일이 아니라', 'Profile · Preset · User Override를 고치고 다시 생성한다.', '', '## 부트스트랩 (모든 Run 공통)', '', '1. 이 파일 → `controller.md` → `state.md` → 자기 Session Contract → CHECKPOINT(있으면)', '2. Session Contract의 Canonical Sources를 **실제로 다시 읽는다**. 세션 파일의 요약을 믿지 않는다', '3. 정본이 기록된 baseline과 다르면 diff를 확인하고, 계약에 영향이 있으면 중단·보고한다', '', '## 절대 규칙', '', '- 이 목록의 행위는 Session Contract로 열 수 없다. Policy Exception으로도 열리지 않는다:', ...resolved.policy.hardDeny.map((item) => ` - \`${item}\``), '- 외부로 나가는 길은 Execution Grant 하나뿐이다. 승인은 게시 권한이 아니다', '- 미결은 임의로 확정하지 않는다 — UNRESOLVED로 회수한다', '- Verifier는 발견하고 반환한다. 고치지 않는다', '', '## Controller 허가가 필요한 행위', '', resolved.policy.softDeny.length > 0
|
|
15
|
+
? '아래는 기본 금지다. 이번 세션에 한해 Controller가 Policy Exception을 주면 열린다:'
|
|
16
|
+
: '_없음_', ...resolved.policy.softDeny.map((item) => `- \`${item}\``), '', '## Role별 쓰기 범위', '', '| Role | 최대 범위 |', '|---|---|', ...Object.entries(resolved.policy.roleScopes).map(([role, scopes]) => `| ${role} | ${scopes.length > 0 ? scopes.map((s) => `\`${s}\``).join(', ') : '_없음_'} |`), '', 'Session Contract는 이보다 **좁아야** 한다. 넓은 범위를 요청하면 발급이 거절된다.', '', '## 정본 (Canonical Sources)', '', '| id | provider | ref | 경로 |', '|---|---|---|---|', ...profile.canonical.sources.map((s) => `| ${s.id} | ${s.provider} | ${s.ref ?? '—'} | ${s.paths.length > 0 ? s.paths.join(', ') : '—'} |`), '', '스냅샷은 source별로 따로 기록한다. 하나로 뭉뚱그리지 않는다.', '', '## 파일별 쓰기 주체', '', '| 파일 | 쓰는 주체 |', '|---|---|', '| `ASC.md`, `cache/resolved-profile`, `profile.lock` | Resolver (생성물) |', '| `controller.md` | 사람 |', '| `state.md`, `blocks/`, `monitor/queue` | Controller 전용 |', '| `sessions/S-*` | 해당 Logical Session |', '| `monitor/M-*`, `monitor/inbox/`, `log-current` | Monitor / 처분 주체 |', '', '세션이 끝나면 Agent는 자기 세션 파일에 Handoff를 쓰는 데까지다.', 'state·block·queue 갱신은 Controller가 회수한 뒤에 한다.', '');
|
|
17
|
+
if (runtime.ownership) {
|
|
18
|
+
const domains = decisionDomains(runtime.ownership);
|
|
19
|
+
lines.push('## 책임 지도 (Ownership)', '', '| 역할 | 쓰기 영역 |', '|---|---|', ...Object.entries(runtime.ownership).map(([role, spec]) => `| ${role} | ${spec.paths.length > 0 ? spec.paths.map((p) => `\`${p}\``).join(', ') : '_없음 (결정만)_'} |`), '', '| 결정 영역 | 결정권자 |', '|---|---|', ...domains.map((domain) => {
|
|
20
|
+
const found = lookupAuthority(runtime.ownership, domain);
|
|
21
|
+
// domain 목록을 이 map에서 뽑았으므로 UNDECLARED는 나올 수 없다. 그래도 조용히
|
|
22
|
+
// 넘기지 않는다 — 표에 빈칸이 뜨면 "결정권자가 없다"로 읽힌다.
|
|
23
|
+
const who = found.kind === 'RESOLVED'
|
|
24
|
+
? found.role
|
|
25
|
+
: found.kind === 'AMBIGUOUS'
|
|
26
|
+
? `**갈림 — ${found.candidates.join(' / ')}**`
|
|
27
|
+
: '_선언 없음_';
|
|
28
|
+
return `| ${domain} | ${who} |`;
|
|
29
|
+
}), '', '결정권자가 갈린 영역은 프로젝트가 아직 정하지 않은 것이다. ASC는 그중 하나를 고르지', '않는다 — 그 결정이 필요한 세션은 발급 전에 멈춘다.', '');
|
|
30
|
+
}
|
|
31
|
+
if (Object.keys(profile.terminology).length > 0) {
|
|
32
|
+
lines.push('## 이 프로젝트의 말', '', '| Core 개념 | 이 프로젝트에서 |', '|---|---|');
|
|
33
|
+
for (const [core, local] of Object.entries(profile.terminology))
|
|
34
|
+
lines.push(`| ${core} | ${local} |`);
|
|
35
|
+
lines.push('');
|
|
36
|
+
}
|
|
37
|
+
if (runtime.degraded.length > 0) {
|
|
38
|
+
lines.push('## 지금 꺼져 있는 기능', '', ...runtime.degraded.map((cap) => `- \`${cap}\` — 제공하는 Adapter가 없다`), '', '없어도 Local 경로만으로 승인은 완결된다.', '');
|
|
39
|
+
}
|
|
40
|
+
return lines.join('\n');
|
|
41
|
+
}
|
|
42
|
+
/** 사람이 처음 채우는 파일. 한 번 만들고 나면 Resolver가 손대지 않는다. */
|
|
43
|
+
export function renderControllerMd(profileId) {
|
|
44
|
+
return [
|
|
45
|
+
`# Controller Contract — ${profileId}`,
|
|
46
|
+
'',
|
|
47
|
+
'사람이 쓰는 파일이다. Resolver 입력이 아니므로 여기를 고쳐도 ASC.md는 다시 생성되지 않는다.',
|
|
48
|
+
'',
|
|
49
|
+
'## 현재 목표',
|
|
50
|
+
'- (여기에 지금 무엇을 하려는지 적는다)',
|
|
51
|
+
'',
|
|
52
|
+
'## 우선순위',
|
|
53
|
+
'- (포인터로 적는다. 내용을 복사하지 않는다)',
|
|
54
|
+
'',
|
|
55
|
+
'## 이번에 허용한 Policy Exception',
|
|
56
|
+
'- (세션 id와 항목을 함께 적는다. 없으면 비워 둔다)',
|
|
57
|
+
'',
|
|
58
|
+
'## Controller Attention',
|
|
59
|
+
'- ',
|
|
60
|
+
'',
|
|
61
|
+
].join('\n');
|
|
62
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type PolicyLayer, type PolicyViolation, type ResolvedPolicy } from '../policy/policy.ts';
|
|
2
|
+
/** 계층 종류. 순서가 곧 우선순위이며, 아래로 갈수록 하위 계층이다 (OM §4.1). */
|
|
3
|
+
export declare const LAYER_ORDER: readonly ["vanilla", "profile", "preset", "override"];
|
|
4
|
+
export type LayerKind = (typeof LAYER_ORDER)[number];
|
|
5
|
+
export type ConfigLayer = PolicyLayer & {
|
|
6
|
+
kind: LayerKind;
|
|
7
|
+
/** Core 호환 요구 (OM §4.10). 형식은 B-10에서 semver로 확정한다. */
|
|
8
|
+
requires?: {
|
|
9
|
+
asc?: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export type ResolvedProfile = {
|
|
13
|
+
policy: ResolvedPolicy;
|
|
14
|
+
/** 실제로 켜진 capability — required 전부 + 제공되는 optional. */
|
|
15
|
+
capabilities: string[];
|
|
16
|
+
/** 요구했지만 Adapter가 제공하지 못해 꺼진 것. 기능 비활성 사유로 사람에게 보인다. */
|
|
17
|
+
degradedCapabilities: string[];
|
|
18
|
+
};
|
|
19
|
+
export type ResolveFailure = {
|
|
20
|
+
kind: 'LAYER_ORDER';
|
|
21
|
+
detail: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'MISSING_CAPABILITY';
|
|
24
|
+
detail: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'POLICY';
|
|
27
|
+
violation: PolicyViolation;
|
|
28
|
+
};
|
|
29
|
+
export type ResolveResult = {
|
|
30
|
+
ok: true;
|
|
31
|
+
profile: ResolvedProfile;
|
|
32
|
+
degraded: string[];
|
|
33
|
+
} | {
|
|
34
|
+
ok: false;
|
|
35
|
+
failures: ResolveFailure[];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* 계층을 병합해 Resolved Profile을 만든다.
|
|
39
|
+
* @param layers 상위→하위 순으로 정렬되어 있어야 한다. 어긋나면 실패로 보고한다.
|
|
40
|
+
* @param available Adapter가 실제로 제공하는 capability 집합.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveProfile(layers: readonly ConfigLayer[], available?: readonly string[]): ResolveResult;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Profile Resolver 골격 — 구성 계층을 정해진 순서로 합쳐 Resolved Profile을 만든다
|
|
2
|
+
// (OM §4.1·§4.7·§4.8). Resolved Profile은 Derived View다: 지워도 다시 만들 수 있어야 하고,
|
|
3
|
+
// 누구도 직접 손대지 않는다.
|
|
4
|
+
//
|
|
5
|
+
// 여기 없는 것: profile.lock 기록·대조(B-10), YAML 로딩(B-10), ASC.md 생성(B-10).
|
|
6
|
+
// 이 단계의 목적은 계층 순서와 병합·검증 규칙이 실제로 성립하는지 확인하는 것이다.
|
|
7
|
+
import { mergePolicyLayers } from "../policy/policy.js";
|
|
8
|
+
/** 계층 종류. 순서가 곧 우선순위이며, 아래로 갈수록 하위 계층이다 (OM §4.1). */
|
|
9
|
+
export const LAYER_ORDER = ['vanilla', 'profile', 'preset', 'override'];
|
|
10
|
+
/**
|
|
11
|
+
* 계층을 병합해 Resolved Profile을 만든다.
|
|
12
|
+
* @param layers 상위→하위 순으로 정렬되어 있어야 한다. 어긋나면 실패로 보고한다.
|
|
13
|
+
* @param available Adapter가 실제로 제공하는 capability 집합.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveProfile(layers, available = []) {
|
|
16
|
+
const failures = [];
|
|
17
|
+
let previous = -1;
|
|
18
|
+
for (const layer of layers) {
|
|
19
|
+
const rank = LAYER_ORDER.indexOf(layer.kind);
|
|
20
|
+
if (rank < previous) {
|
|
21
|
+
failures.push({
|
|
22
|
+
kind: 'LAYER_ORDER',
|
|
23
|
+
detail: `'${layer.id}' (${layer.kind}) appears after a lower-precedence layer`,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
previous = Math.max(previous, rank);
|
|
27
|
+
}
|
|
28
|
+
const { policy, violations } = mergePolicyLayers(layers);
|
|
29
|
+
for (const violation of violations)
|
|
30
|
+
failures.push({ kind: 'POLICY', violation });
|
|
31
|
+
const provided = new Set(available);
|
|
32
|
+
// required 미충족은 bootstrap 실패다 — 없는 기능을 있는 척 굴리지 않는다 (OM §4.7).
|
|
33
|
+
for (const capability of policy.requiredCapabilities) {
|
|
34
|
+
if (!provided.has(capability)) {
|
|
35
|
+
failures.push({ kind: 'MISSING_CAPABILITY', detail: `required capability '${capability}' is not available` });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// optional은 없으면 그 기능만 끈다. Messenger가 없어도 Local 경로는 살아 있어야 한다 (OM §11.3).
|
|
39
|
+
const degradedCapabilities = policy.optionalCapabilities.filter((capability) => !provided.has(capability));
|
|
40
|
+
if (failures.length > 0)
|
|
41
|
+
return { ok: false, failures };
|
|
42
|
+
const capabilities = [
|
|
43
|
+
...policy.requiredCapabilities,
|
|
44
|
+
...policy.optionalCapabilities.filter((capability) => provided.has(capability)),
|
|
45
|
+
];
|
|
46
|
+
return {
|
|
47
|
+
ok: true,
|
|
48
|
+
profile: { policy, capabilities, degradedCapabilities },
|
|
49
|
+
degraded: degradedCapabilities,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type Version = readonly [number, number, number];
|
|
2
|
+
export declare function parseVersion(text: string): Version | null;
|
|
3
|
+
export type RangeCheck = {
|
|
4
|
+
ok: true;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
reason: 'UNPARSEABLE_RANGE';
|
|
8
|
+
detail: string;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
reason: 'UNPARSEABLE_VERSION';
|
|
12
|
+
detail: string;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
reason: 'OUT_OF_RANGE';
|
|
16
|
+
detail: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* `version`이 `range`를 만족하는가.
|
|
20
|
+
* 판정할 수 없으면 만족한 것으로 넘기지 않는다 — 모르면 막는 쪽이 맞다.
|
|
21
|
+
*/
|
|
22
|
+
export declare function satisfies(version: string, range: string): RangeCheck;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Profile이 요구하는 Core 버전 범위를 판정한다 (OM §4.10).
|
|
2
|
+
//
|
|
3
|
+
// 전체 semver range 문법(캐럿·틸드·`||`·x-range)을 지원하지 않는다. Profile이 적는 것은
|
|
4
|
+
// "이 범위의 Core에서 돌아간다"는 하한과 상한이고, 그 이상은 필요하지 않았다.
|
|
5
|
+
// 대신 문법 밖은 통과시키지 않고 실패로 돌려준다 — 읽지 못한 요구를 만족한 것으로
|
|
6
|
+
// 넘기면 호환되지 않는 Core에서 그냥 돌아가 버린다.
|
|
7
|
+
//
|
|
8
|
+
// 지원: `>=0.1`, `<1.0`, `>1.2.3`, `<=2.0`, `=1.0.0`, 그리고 공백으로 이어 붙인 AND.
|
|
9
|
+
export function parseVersion(text) {
|
|
10
|
+
const match = /^(\d+)\.(\d+)(?:\.(\d+))?$/.exec(text.trim());
|
|
11
|
+
if (!match)
|
|
12
|
+
return null;
|
|
13
|
+
return [Number(match[1]), Number(match[2]), Number(match[3] ?? 0)];
|
|
14
|
+
}
|
|
15
|
+
function compare(a, b) {
|
|
16
|
+
for (let i = 0; i < 3; i++) {
|
|
17
|
+
if (a[i] !== b[i])
|
|
18
|
+
return a[i] < b[i] ? -1 : 1;
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `version`이 `range`를 만족하는가.
|
|
24
|
+
* 판정할 수 없으면 만족한 것으로 넘기지 않는다 — 모르면 막는 쪽이 맞다.
|
|
25
|
+
*/
|
|
26
|
+
export function satisfies(version, range) {
|
|
27
|
+
const current = parseVersion(version);
|
|
28
|
+
if (!current)
|
|
29
|
+
return { ok: false, reason: 'UNPARSEABLE_VERSION', detail: version };
|
|
30
|
+
const clauses = range.trim().split(/\s+/).filter(Boolean);
|
|
31
|
+
if (clauses.length === 0)
|
|
32
|
+
return { ok: false, reason: 'UNPARSEABLE_RANGE', detail: range };
|
|
33
|
+
for (const clause of clauses) {
|
|
34
|
+
const match = /^(>=|<=|>|<|=)?\s*(\d+\.\d+(?:\.\d+)?)$/.exec(clause);
|
|
35
|
+
if (!match)
|
|
36
|
+
return { ok: false, reason: 'UNPARSEABLE_RANGE', detail: clause };
|
|
37
|
+
const operator = match[1] ?? '=';
|
|
38
|
+
const bound = parseVersion(match[2]);
|
|
39
|
+
const order = compare(current, bound);
|
|
40
|
+
const satisfied = operator === '>=' ? order >= 0
|
|
41
|
+
: operator === '<=' ? order <= 0
|
|
42
|
+
: operator === '>' ? order > 0
|
|
43
|
+
: operator === '<' ? order < 0
|
|
44
|
+
: order === 0;
|
|
45
|
+
if (!satisfied)
|
|
46
|
+
return { ok: false, reason: 'OUT_OF_RANGE', detail: `${version} 은 ${clause} 를 만족하지 않는다` };
|
|
47
|
+
}
|
|
48
|
+
return { ok: true };
|
|
49
|
+
}
|