@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,82 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** `W-` + 32 hex. 날짜·순번을 쓰지 않는다 — 두 기계가 같은 날 만든 id가 부딪히면 안 된다. */
|
|
3
|
+
export declare const WORKSPACE_ID: RegExp;
|
|
4
|
+
export declare const WorkspaceId: z.ZodString;
|
|
5
|
+
/** 팀이 채택했는가, 개인이 쓰는가 (C-11 §2). **추론으로 승격되지 않는다.** */
|
|
6
|
+
export declare const AdoptionScope: z.ZodEnum<["local", "project"]>;
|
|
7
|
+
export type AdoptionScope = z.infer<typeof AdoptionScope>;
|
|
8
|
+
export declare const Locator: z.ZodObject<{
|
|
9
|
+
/** 이 기계의 checkout 경로. 정규화해 저장한다 — 대소문자·구분자 차이로 갈라지지 않게. */
|
|
10
|
+
path: z.ZodString;
|
|
11
|
+
/** 이 경로가 worktree인가 main checkout인가. 모르면 적지 않는다. */
|
|
12
|
+
kind: z.ZodOptional<z.ZodEnum<["checkout", "worktree"]>>;
|
|
13
|
+
platform: z.ZodString;
|
|
14
|
+
observedAt: z.ZodString;
|
|
15
|
+
}, "strip", z.ZodTypeAny, {
|
|
16
|
+
path: string;
|
|
17
|
+
observedAt: string;
|
|
18
|
+
platform: string;
|
|
19
|
+
kind?: "checkout" | "worktree" | undefined;
|
|
20
|
+
}, {
|
|
21
|
+
path: string;
|
|
22
|
+
observedAt: string;
|
|
23
|
+
platform: string;
|
|
24
|
+
kind?: "checkout" | "worktree" | undefined;
|
|
25
|
+
}>;
|
|
26
|
+
export type Locator = z.infer<typeof Locator>;
|
|
27
|
+
export declare const Workspace: z.ZodObject<{
|
|
28
|
+
workspaceId: z.ZodString;
|
|
29
|
+
/** 정규화된 외부 이름들. `host/group/project` 형태이며 자격·포트·질의는 들어가지 않는다. */
|
|
30
|
+
aliases: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
31
|
+
adoptionScope: z.ZodDefault<z.ZodEnum<["local", "project"]>>;
|
|
32
|
+
createdAt: z.ZodString;
|
|
33
|
+
lastSeenAt: z.ZodString;
|
|
34
|
+
}, "strip", z.ZodTypeAny, {
|
|
35
|
+
lastSeenAt: string;
|
|
36
|
+
workspaceId: string;
|
|
37
|
+
aliases: string[];
|
|
38
|
+
adoptionScope: "local" | "project";
|
|
39
|
+
createdAt: string;
|
|
40
|
+
}, {
|
|
41
|
+
lastSeenAt: string;
|
|
42
|
+
workspaceId: string;
|
|
43
|
+
createdAt: string;
|
|
44
|
+
aliases?: string[] | undefined;
|
|
45
|
+
adoptionScope?: "local" | "project" | undefined;
|
|
46
|
+
}>;
|
|
47
|
+
export type Workspace = z.infer<typeof Workspace>;
|
|
48
|
+
export declare const newWorkspaceId: () => string;
|
|
49
|
+
/**
|
|
50
|
+
* remote URL을 identity alias로 정규화한다 (C-11 §1.2).
|
|
51
|
+
*
|
|
52
|
+
* 자격·userinfo·포트·질의는 **넣지 않는다.** 비밀이 identity에 섞이면 index와 로그에
|
|
53
|
+
* 비밀이 남는다. 형태가 달라도 같은 저장소면 같은 문자열이 나와야 한다:
|
|
54
|
+
*
|
|
55
|
+
* git@host:group/project.git
|
|
56
|
+
* https://user:token@host:8443/group/project.git?ref=main
|
|
57
|
+
* ssh://git@host:2222/group/project
|
|
58
|
+
* → host/group/project
|
|
59
|
+
*/
|
|
60
|
+
export declare function normalizeRemote(url: string): string | null;
|
|
61
|
+
/**
|
|
62
|
+
* 경로를 비교 가능한 형태로. Windows의 역슬래시·대소문자와 후행 구분자를 흡수한다.
|
|
63
|
+
*
|
|
64
|
+
* **경로는 identity가 아니다** — 이 값은 locator 조회 키일 뿐이며, 이동하면 바뀐다.
|
|
65
|
+
*/
|
|
66
|
+
export declare function normalizeLocator(path: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* 같은 workspace로 볼 근거가 있는가 (C-11 §1.1).
|
|
69
|
+
*
|
|
70
|
+
* **동일성 증명이 아니다.** alias가 겹치면 "그 프로젝트일 수 있다"까지이며, 실제로
|
|
71
|
+
* 이어붙일지는 호출자가 정한다. remote가 하나도 없으면 후보가 아니다 — 없는 근거로
|
|
72
|
+
* 이어붙이는 것이 경로를 identity로 쓰는 것과 같은 실수다.
|
|
73
|
+
*/
|
|
74
|
+
export declare function recoverCandidates(workspaces: readonly Workspace[], remotes: readonly string[]): {
|
|
75
|
+
workspace: Workspace;
|
|
76
|
+
matched: string[];
|
|
77
|
+
}[];
|
|
78
|
+
/** 사람이 읽는 줄. 후보가 여럿이면 고르지 않고 여럿이라고 말한다. */
|
|
79
|
+
export declare function recoverLines(hits: readonly {
|
|
80
|
+
workspace: Workspace;
|
|
81
|
+
matched: string[];
|
|
82
|
+
}[]): string[];
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Workspace Identity — 이것이 어떤 논리적 프로젝트인가 (C-11 §1).
|
|
2
|
+
//
|
|
3
|
+
// 지금까지 identity는 사실상 "walk-up이 멈춘 경로"였다. 그러면 디렉터리를 한 번 옮기는
|
|
4
|
+
// 것만으로 다른 프로젝트가 되고, 같은 저장소를 두 곳에 clone하면 서로를 알아보지 못한다.
|
|
5
|
+
//
|
|
6
|
+
// 네 개념을 분리한다:
|
|
7
|
+
//
|
|
8
|
+
// Workspace Identity 안정적인 내부 id. 사람이 바꾸지 않는다
|
|
9
|
+
// Alias 외부 시스템이 부르는 이름들. 여러 개고 바뀔 수 있다
|
|
10
|
+
// Locator 이 기계에서 지금 어디에 있는가. 자주 바뀐다
|
|
11
|
+
// Execution Instance 그 Locator에서 실제로 도는 작업 단위 (worktree)
|
|
12
|
+
//
|
|
13
|
+
// 내부 id가 정본인 이유: remote가 없는 저장소도, remote가 바뀌는 저장소도 있어야 하기
|
|
14
|
+
// 때문이다. alias가 필요한 이유: 새 clone을 같은 프로젝트로 알아보는 유일한 단서이기
|
|
15
|
+
// 때문이다. **alias 일치는 recover candidate이지 동일성 증명이 아니다** (C-11 불변식 ③).
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
/** `W-` + 32 hex. 날짜·순번을 쓰지 않는다 — 두 기계가 같은 날 만든 id가 부딪히면 안 된다. */
|
|
19
|
+
export const WORKSPACE_ID = /^W-[0-9a-f]{32}$/;
|
|
20
|
+
export const WorkspaceId = z.string().regex(WORKSPACE_ID);
|
|
21
|
+
/** 팀이 채택했는가, 개인이 쓰는가 (C-11 §2). **추론으로 승격되지 않는다.** */
|
|
22
|
+
export const AdoptionScope = z.enum(['local', 'project']);
|
|
23
|
+
export const Locator = z.object({
|
|
24
|
+
/** 이 기계의 checkout 경로. 정규화해 저장한다 — 대소문자·구분자 차이로 갈라지지 않게. */
|
|
25
|
+
path: z.string().min(1),
|
|
26
|
+
/** 이 경로가 worktree인가 main checkout인가. 모르면 적지 않는다. */
|
|
27
|
+
kind: z.enum(['checkout', 'worktree']).optional(),
|
|
28
|
+
platform: z.string().min(1),
|
|
29
|
+
observedAt: z.string().min(1),
|
|
30
|
+
});
|
|
31
|
+
export const Workspace = z.object({
|
|
32
|
+
workspaceId: WorkspaceId,
|
|
33
|
+
/** 정규화된 외부 이름들. `host/group/project` 형태이며 자격·포트·질의는 들어가지 않는다. */
|
|
34
|
+
aliases: z.array(z.string()).default([]),
|
|
35
|
+
adoptionScope: AdoptionScope.default('local'),
|
|
36
|
+
createdAt: z.string().min(1),
|
|
37
|
+
lastSeenAt: z.string().min(1),
|
|
38
|
+
});
|
|
39
|
+
export const newWorkspaceId = () => `W-${randomUUID().replace(/-/g, '')}`;
|
|
40
|
+
/**
|
|
41
|
+
* remote URL을 identity alias로 정규화한다 (C-11 §1.2).
|
|
42
|
+
*
|
|
43
|
+
* 자격·userinfo·포트·질의는 **넣지 않는다.** 비밀이 identity에 섞이면 index와 로그에
|
|
44
|
+
* 비밀이 남는다. 형태가 달라도 같은 저장소면 같은 문자열이 나와야 한다:
|
|
45
|
+
*
|
|
46
|
+
* git@host:group/project.git
|
|
47
|
+
* https://user:token@host:8443/group/project.git?ref=main
|
|
48
|
+
* ssh://git@host:2222/group/project
|
|
49
|
+
* → host/group/project
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeRemote(url) {
|
|
52
|
+
const trimmed = url.trim();
|
|
53
|
+
if (!trimmed)
|
|
54
|
+
return null;
|
|
55
|
+
// 로컬 경로는 identity가 아니다. `C:\work\repo` 는 scp 문법(`host:path`)과 모양이 같아
|
|
56
|
+
// 그냥 두면 드라이브 문자가 host가 된다 — 실측에서 실제로 그렇게 나왔다.
|
|
57
|
+
if (/^[a-zA-Z]:[\\/]/.test(trimmed) || trimmed.startsWith('/') || trimmed.startsWith('file://'))
|
|
58
|
+
return null;
|
|
59
|
+
let host;
|
|
60
|
+
let path;
|
|
61
|
+
const scheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/([^/]+)\/(.+)$/.exec(trimmed);
|
|
62
|
+
if (scheme) {
|
|
63
|
+
host = scheme[1];
|
|
64
|
+
path = scheme[2];
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// scp 문법 — `user@host:group/project`. 콜론 뒤가 경로다.
|
|
68
|
+
const scp = /^(?:[^@/]+@)?([^:/]+):(.+)$/.exec(trimmed);
|
|
69
|
+
if (scp) {
|
|
70
|
+
host = scp[1];
|
|
71
|
+
path = scp[2];
|
|
72
|
+
}
|
|
73
|
+
else if (/^[^/]+\/.+$/.test(trimmed)) {
|
|
74
|
+
// 이미 `host/group/project` 형태로 정규화된 값
|
|
75
|
+
const slash = trimmed.indexOf('/');
|
|
76
|
+
host = trimmed.slice(0, slash);
|
|
77
|
+
path = trimmed.slice(slash + 1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!host || !path)
|
|
81
|
+
return null;
|
|
82
|
+
// userinfo·포트를 떼고 host만 남긴다. 대소문자는 host에서 의미가 없다.
|
|
83
|
+
const bare = host.slice(host.lastIndexOf('@') + 1).replace(/:\d+$/, '').toLowerCase();
|
|
84
|
+
const cleaned = path
|
|
85
|
+
.split(/[?#]/)[0]
|
|
86
|
+
.replace(/\.git$/, '')
|
|
87
|
+
.replace(/^\/+|\/+$/g, '');
|
|
88
|
+
if (!bare || !cleaned)
|
|
89
|
+
return null;
|
|
90
|
+
return `${bare}/${cleaned}`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 경로를 비교 가능한 형태로. Windows의 역슬래시·대소문자와 후행 구분자를 흡수한다.
|
|
94
|
+
*
|
|
95
|
+
* **경로는 identity가 아니다** — 이 값은 locator 조회 키일 뿐이며, 이동하면 바뀐다.
|
|
96
|
+
*/
|
|
97
|
+
export function normalizeLocator(path) {
|
|
98
|
+
const slashed = path.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
99
|
+
// 드라이브 문자만 대소문자를 흡수한다. 나머지는 그대로 둔다 — 대소문자를 구분하는
|
|
100
|
+
// 파일시스템에서 서로 다른 두 디렉터리를 하나로 합치면 그게 더 큰 사고다.
|
|
101
|
+
return /^[a-zA-Z]:/.test(slashed) ? slashed[0].toUpperCase() + slashed.slice(1) : slashed;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 같은 workspace로 볼 근거가 있는가 (C-11 §1.1).
|
|
105
|
+
*
|
|
106
|
+
* **동일성 증명이 아니다.** alias가 겹치면 "그 프로젝트일 수 있다"까지이며, 실제로
|
|
107
|
+
* 이어붙일지는 호출자가 정한다. remote가 하나도 없으면 후보가 아니다 — 없는 근거로
|
|
108
|
+
* 이어붙이는 것이 경로를 identity로 쓰는 것과 같은 실수다.
|
|
109
|
+
*/
|
|
110
|
+
export function recoverCandidates(workspaces, remotes) {
|
|
111
|
+
const normalized = remotes.map(normalizeRemote).filter((alias) => alias !== null);
|
|
112
|
+
if (normalized.length === 0)
|
|
113
|
+
return [];
|
|
114
|
+
return workspaces
|
|
115
|
+
.map((workspace) => ({
|
|
116
|
+
workspace,
|
|
117
|
+
matched: workspace.aliases.filter((alias) => normalized.includes(alias)),
|
|
118
|
+
}))
|
|
119
|
+
.filter((hit) => hit.matched.length > 0);
|
|
120
|
+
}
|
|
121
|
+
/** 사람이 읽는 줄. 후보가 여럿이면 고르지 않고 여럿이라고 말한다. */
|
|
122
|
+
export function recoverLines(hits) {
|
|
123
|
+
if (hits.length === 0)
|
|
124
|
+
return ['알아볼 수 있는 workspace 없음 — 새로 만들거나 --root 로 지정하라'];
|
|
125
|
+
if (hits.length === 1) {
|
|
126
|
+
const [only] = hits;
|
|
127
|
+
return [`후보 1개: ${only.workspace.workspaceId} (일치 ${only.matched.join(', ')})`];
|
|
128
|
+
}
|
|
129
|
+
return [
|
|
130
|
+
`후보가 ${hits.length}개다 — 고르지 않는다. 사람이 정하라:`,
|
|
131
|
+
...hits.map((hit) => ` ${hit.workspace.workspaceId} (일치 ${hit.matched.join(', ')})`),
|
|
132
|
+
];
|
|
133
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type Locator } from './identity.ts';
|
|
3
|
+
export declare const WORKSPACE_INDEX_FILE = "workspace-index.json";
|
|
4
|
+
declare const LocatorEntry: z.ZodObject<{
|
|
5
|
+
workspaceId: z.ZodString;
|
|
6
|
+
/** 이 locator에 붙는 ASC runtime 뿌리. guard가 이 값만 있으면 판정할 수 있다. */
|
|
7
|
+
root: z.ZodString;
|
|
8
|
+
kind: z.ZodOptional<z.ZodEnum<["checkout", "worktree"]>>;
|
|
9
|
+
platform: z.ZodString;
|
|
10
|
+
observedAt: z.ZodString;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
observedAt: string;
|
|
13
|
+
root: string;
|
|
14
|
+
platform: string;
|
|
15
|
+
workspaceId: string;
|
|
16
|
+
kind?: "checkout" | "worktree" | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
observedAt: string;
|
|
19
|
+
root: string;
|
|
20
|
+
platform: string;
|
|
21
|
+
workspaceId: string;
|
|
22
|
+
kind?: "checkout" | "worktree" | undefined;
|
|
23
|
+
}>;
|
|
24
|
+
export type LocatorEntry = z.infer<typeof LocatorEntry>;
|
|
25
|
+
export declare const WorkspaceIndex: z.ZodObject<{
|
|
26
|
+
version: z.ZodLiteral<1>;
|
|
27
|
+
workspaces: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
28
|
+
workspaceId: z.ZodString;
|
|
29
|
+
aliases: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
30
|
+
adoptionScope: z.ZodDefault<z.ZodEnum<["local", "project"]>>;
|
|
31
|
+
createdAt: z.ZodString;
|
|
32
|
+
lastSeenAt: z.ZodString;
|
|
33
|
+
}, "strip", z.ZodTypeAny, {
|
|
34
|
+
lastSeenAt: string;
|
|
35
|
+
workspaceId: string;
|
|
36
|
+
aliases: string[];
|
|
37
|
+
adoptionScope: "local" | "project";
|
|
38
|
+
createdAt: string;
|
|
39
|
+
}, {
|
|
40
|
+
lastSeenAt: string;
|
|
41
|
+
workspaceId: string;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
aliases?: string[] | undefined;
|
|
44
|
+
adoptionScope?: "local" | "project" | undefined;
|
|
45
|
+
}>>>;
|
|
46
|
+
/** 정규화된 경로 → workspace. guard가 cwd에서 위로 올라가며 찾는 표다. */
|
|
47
|
+
locators: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
48
|
+
workspaceId: z.ZodString;
|
|
49
|
+
/** 이 locator에 붙는 ASC runtime 뿌리. guard가 이 값만 있으면 판정할 수 있다. */
|
|
50
|
+
root: z.ZodString;
|
|
51
|
+
kind: z.ZodOptional<z.ZodEnum<["checkout", "worktree"]>>;
|
|
52
|
+
platform: z.ZodString;
|
|
53
|
+
observedAt: z.ZodString;
|
|
54
|
+
}, "strip", z.ZodTypeAny, {
|
|
55
|
+
observedAt: string;
|
|
56
|
+
root: string;
|
|
57
|
+
platform: string;
|
|
58
|
+
workspaceId: string;
|
|
59
|
+
kind?: "checkout" | "worktree" | undefined;
|
|
60
|
+
}, {
|
|
61
|
+
observedAt: string;
|
|
62
|
+
root: string;
|
|
63
|
+
platform: string;
|
|
64
|
+
workspaceId: string;
|
|
65
|
+
kind?: "checkout" | "worktree" | undefined;
|
|
66
|
+
}>>>;
|
|
67
|
+
}, "strip", z.ZodTypeAny, {
|
|
68
|
+
version: 1;
|
|
69
|
+
workspaces: Record<string, {
|
|
70
|
+
lastSeenAt: string;
|
|
71
|
+
workspaceId: string;
|
|
72
|
+
aliases: string[];
|
|
73
|
+
adoptionScope: "local" | "project";
|
|
74
|
+
createdAt: string;
|
|
75
|
+
}>;
|
|
76
|
+
locators: Record<string, {
|
|
77
|
+
observedAt: string;
|
|
78
|
+
root: string;
|
|
79
|
+
platform: string;
|
|
80
|
+
workspaceId: string;
|
|
81
|
+
kind?: "checkout" | "worktree" | undefined;
|
|
82
|
+
}>;
|
|
83
|
+
}, {
|
|
84
|
+
version: 1;
|
|
85
|
+
workspaces?: Record<string, {
|
|
86
|
+
lastSeenAt: string;
|
|
87
|
+
workspaceId: string;
|
|
88
|
+
createdAt: string;
|
|
89
|
+
aliases?: string[] | undefined;
|
|
90
|
+
adoptionScope?: "local" | "project" | undefined;
|
|
91
|
+
}> | undefined;
|
|
92
|
+
locators?: Record<string, {
|
|
93
|
+
observedAt: string;
|
|
94
|
+
root: string;
|
|
95
|
+
platform: string;
|
|
96
|
+
workspaceId: string;
|
|
97
|
+
kind?: "checkout" | "worktree" | undefined;
|
|
98
|
+
}> | undefined;
|
|
99
|
+
}>;
|
|
100
|
+
export type WorkspaceIndex = z.infer<typeof WorkspaceIndex>;
|
|
101
|
+
export declare const emptyIndex: () => WorkspaceIndex;
|
|
102
|
+
/**
|
|
103
|
+
* index를 읽는다. 없으면 빈 index다 — 없는 것과 깨진 것을 구분하지 않는 쪽이 위험하므로
|
|
104
|
+
* 깨진 파일은 던진다. 조용히 빈 index로 시작하면 등록된 workspace가 통째로 사라진 것처럼
|
|
105
|
+
* 보이고, 그 위에 새로 쓰면 진짜로 사라진다.
|
|
106
|
+
*/
|
|
107
|
+
export declare function readIndex(homeDir: string): Promise<WorkspaceIndex>;
|
|
108
|
+
/** tmp+rename. 같은 디렉터리 안에서 rename해야 원자성이 선다. */
|
|
109
|
+
export declare function writeIndex(homeDir: string, index: WorkspaceIndex): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* cwd에서 위로 올라가며 이 경로가 속한 workspace를 찾는다.
|
|
112
|
+
*
|
|
113
|
+
* **경계가 있다.** 등록된 locator에만 반응하므로, 상위 어딘가에 우연히 있는 디렉터리를
|
|
114
|
+
* 프로젝트 상태로 오인하지 않는다 — 무한 walk-up이 만들던 문제가 여기서는 생기지 않는다.
|
|
115
|
+
*/
|
|
116
|
+
export declare function lookupLocator(index: WorkspaceIndex, cwd: string): (LocatorEntry & {
|
|
117
|
+
locator: string;
|
|
118
|
+
}) | null;
|
|
119
|
+
export type RegisterInput = {
|
|
120
|
+
workspaceId: string;
|
|
121
|
+
root: string;
|
|
122
|
+
locator: Locator;
|
|
123
|
+
aliases?: readonly string[];
|
|
124
|
+
adoptionScope?: 'local' | 'project';
|
|
125
|
+
now: string;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* workspace와 locator를 등록한다. **기존 alias를 지우지 않는다** — provider가 바뀌어도
|
|
129
|
+
* 옛 이름으로 알아볼 수 있어야 하고(C-11 불변식 ②), 이름이 사라지면 그 근거도 사라진다.
|
|
130
|
+
*
|
|
131
|
+
* 순수 함수다. 저장은 호출자가 writeIndex로 한다 — 읽고-고쳐-쓰기의 경계를 한 군데로 모은다.
|
|
132
|
+
*/
|
|
133
|
+
export declare function register(index: WorkspaceIndex, input: RegisterInput): WorkspaceIndex;
|
|
134
|
+
/** 이 workspace에 달린 locator들. 하나의 논리 workspace가 여러 곳에 있을 수 있다. */
|
|
135
|
+
export declare function locatorsOf(index: WorkspaceIndex, workspaceId: string): (LocatorEntry & {
|
|
136
|
+
locator: string;
|
|
137
|
+
})[];
|
|
138
|
+
/** 더 이상 없는 checkout을 목록에서 뺀다. workspace 자체는 지우지 않는다. */
|
|
139
|
+
export declare function forgetLocator(index: WorkspaceIndex, path: string): WorkspaceIndex;
|
|
140
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Workspace Index — 이 기계의 경로에서 workspace를 되찾는 역색인 (C-11 §3).
|
|
2
|
+
//
|
|
3
|
+
// 저장 위치가 저장소 밖으로 나가면 "여기가 어느 workspace인가"를 경로 탐색만으로는
|
|
4
|
+
// 알 수 없다. 그 답을 이 파일 하나가 진다.
|
|
5
|
+
//
|
|
6
|
+
// **가장 까다로운 독자는 Host guard hook이다.** hook은 어떤 프로젝트에서든 도는 무의존
|
|
7
|
+
// 단일 파일이고, 매 Bash 호출마다 실행된다. 그래서 포맷의 상한이 정해져 있다:
|
|
8
|
+
//
|
|
9
|
+
// readFileSync 한 번 + JSON.parse 한 번 + 문자열 조회
|
|
10
|
+
//
|
|
11
|
+
// 데이터베이스도, 다단 조회도, 스키마 협상도 두지 않는다 (C-11 불변식 ⑩).
|
|
12
|
+
//
|
|
13
|
+
// 쓰기는 tmp+rename이다. 반쯤 쓰인 index를 guard가 읽으면 관리 대상 판정이 흔들린다
|
|
14
|
+
// (C-11 불변식 ⑨).
|
|
15
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
import { Workspace, normalizeLocator } from "./identity.js";
|
|
19
|
+
export const WORKSPACE_INDEX_FILE = 'workspace-index.json';
|
|
20
|
+
const LocatorEntry = z.object({
|
|
21
|
+
workspaceId: z.string().min(1),
|
|
22
|
+
/** 이 locator에 붙는 ASC runtime 뿌리. guard가 이 값만 있으면 판정할 수 있다. */
|
|
23
|
+
root: z.string().min(1),
|
|
24
|
+
kind: z.enum(['checkout', 'worktree']).optional(),
|
|
25
|
+
platform: z.string().min(1),
|
|
26
|
+
observedAt: z.string().min(1),
|
|
27
|
+
});
|
|
28
|
+
export const WorkspaceIndex = z.object({
|
|
29
|
+
version: z.literal(1),
|
|
30
|
+
workspaces: z.record(Workspace).default({}),
|
|
31
|
+
/** 정규화된 경로 → workspace. guard가 cwd에서 위로 올라가며 찾는 표다. */
|
|
32
|
+
locators: z.record(LocatorEntry).default({}),
|
|
33
|
+
});
|
|
34
|
+
export const emptyIndex = () => ({ version: 1, workspaces: {}, locators: {} });
|
|
35
|
+
/**
|
|
36
|
+
* index를 읽는다. 없으면 빈 index다 — 없는 것과 깨진 것을 구분하지 않는 쪽이 위험하므로
|
|
37
|
+
* 깨진 파일은 던진다. 조용히 빈 index로 시작하면 등록된 workspace가 통째로 사라진 것처럼
|
|
38
|
+
* 보이고, 그 위에 새로 쓰면 진짜로 사라진다.
|
|
39
|
+
*/
|
|
40
|
+
export async function readIndex(homeDir) {
|
|
41
|
+
let raw;
|
|
42
|
+
try {
|
|
43
|
+
raw = await readFile(join(homeDir, WORKSPACE_INDEX_FILE), 'utf8');
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code === 'ENOENT')
|
|
47
|
+
return emptyIndex();
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
return WorkspaceIndex.parse(JSON.parse(raw));
|
|
51
|
+
}
|
|
52
|
+
/** tmp+rename. 같은 디렉터리 안에서 rename해야 원자성이 선다. */
|
|
53
|
+
export async function writeIndex(homeDir, index) {
|
|
54
|
+
const file = join(homeDir, WORKSPACE_INDEX_FILE);
|
|
55
|
+
await mkdir(dirname(file), { recursive: true });
|
|
56
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
57
|
+
await writeFile(tmp, `${JSON.stringify(index, null, 2)}\n`, 'utf8');
|
|
58
|
+
await rename(tmp, file);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* cwd에서 위로 올라가며 이 경로가 속한 workspace를 찾는다.
|
|
62
|
+
*
|
|
63
|
+
* **경계가 있다.** 등록된 locator에만 반응하므로, 상위 어딘가에 우연히 있는 디렉터리를
|
|
64
|
+
* 프로젝트 상태로 오인하지 않는다 — 무한 walk-up이 만들던 문제가 여기서는 생기지 않는다.
|
|
65
|
+
*/
|
|
66
|
+
export function lookupLocator(index, cwd) {
|
|
67
|
+
let path = normalizeLocator(cwd);
|
|
68
|
+
for (;;) {
|
|
69
|
+
const entry = index.locators[path];
|
|
70
|
+
if (entry)
|
|
71
|
+
return { ...entry, locator: path };
|
|
72
|
+
const parent = path.slice(0, path.lastIndexOf('/'));
|
|
73
|
+
// 루트(`C:` 나 빈 문자열)에 닿으면 끝이다
|
|
74
|
+
if (!parent || parent === path || /^[a-zA-Z]:$/.test(path))
|
|
75
|
+
return null;
|
|
76
|
+
path = parent;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* workspace와 locator를 등록한다. **기존 alias를 지우지 않는다** — provider가 바뀌어도
|
|
81
|
+
* 옛 이름으로 알아볼 수 있어야 하고(C-11 불변식 ②), 이름이 사라지면 그 근거도 사라진다.
|
|
82
|
+
*
|
|
83
|
+
* 순수 함수다. 저장은 호출자가 writeIndex로 한다 — 읽고-고쳐-쓰기의 경계를 한 군데로 모은다.
|
|
84
|
+
*/
|
|
85
|
+
export function register(index, input) {
|
|
86
|
+
const existing = index.workspaces[input.workspaceId];
|
|
87
|
+
const aliases = [...new Set([...(existing?.aliases ?? []), ...(input.aliases ?? [])])];
|
|
88
|
+
const workspace = Workspace.parse({
|
|
89
|
+
workspaceId: input.workspaceId,
|
|
90
|
+
aliases,
|
|
91
|
+
// adoption은 추론으로 승격되지 않는다 — 명시적으로 넘길 때만 바뀐다 (C-11 불변식 ⑤)
|
|
92
|
+
adoptionScope: input.adoptionScope ?? existing?.adoptionScope ?? 'local',
|
|
93
|
+
createdAt: existing?.createdAt ?? input.now,
|
|
94
|
+
lastSeenAt: input.now,
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
...index,
|
|
98
|
+
workspaces: { ...index.workspaces, [workspace.workspaceId]: workspace },
|
|
99
|
+
locators: {
|
|
100
|
+
...index.locators,
|
|
101
|
+
[normalizeLocator(input.locator.path)]: LocatorEntry.parse({
|
|
102
|
+
workspaceId: input.workspaceId,
|
|
103
|
+
root: input.root,
|
|
104
|
+
...(input.locator.kind ? { kind: input.locator.kind } : {}),
|
|
105
|
+
platform: input.locator.platform,
|
|
106
|
+
observedAt: input.locator.observedAt,
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** 이 workspace에 달린 locator들. 하나의 논리 workspace가 여러 곳에 있을 수 있다. */
|
|
112
|
+
export function locatorsOf(index, workspaceId) {
|
|
113
|
+
return Object.entries(index.locators)
|
|
114
|
+
.filter(([, entry]) => entry.workspaceId === workspaceId)
|
|
115
|
+
.map(([locator, entry]) => ({ ...entry, locator }));
|
|
116
|
+
}
|
|
117
|
+
/** 더 이상 없는 checkout을 목록에서 뺀다. workspace 자체는 지우지 않는다. */
|
|
118
|
+
export function forgetLocator(index, path) {
|
|
119
|
+
const key = normalizeLocator(path);
|
|
120
|
+
if (!(key in index.locators))
|
|
121
|
+
return index;
|
|
122
|
+
const locators = { ...index.locators };
|
|
123
|
+
delete locators[key];
|
|
124
|
+
return { ...index, locators };
|
|
125
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type Adoption = {
|
|
2
|
+
kind: 'PROJECT_ADOPTED';
|
|
3
|
+
evidence: string;
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'PERSONAL_LEGACY';
|
|
6
|
+
evidence: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'AMBIGUOUS';
|
|
9
|
+
evidence: string;
|
|
10
|
+
};
|
|
11
|
+
export type AdoptionInput = {
|
|
12
|
+
projectRoot: string;
|
|
13
|
+
/** Git이 추적하는 경로들 중 `.asc` 아래인 것. 하나라도 있으면 팀의 것이다. */
|
|
14
|
+
trackedAscPaths: readonly string[];
|
|
15
|
+
/** `.git/info/exclude` 내용. `.asc/` 가 등록돼 있으면 개인용으로 숨긴 것이다. */
|
|
16
|
+
excludeContent?: string;
|
|
17
|
+
/** `.gitignore` 내용. 팀 파일에 `.asc/` 가 있으면 팀이 알고 둔 것이다. */
|
|
18
|
+
gitignoreContent?: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* 이 `.asc/` 가 누구 것인가.
|
|
22
|
+
*
|
|
23
|
+
* 순서가 중요하다. **추적된 파일이 있으면 다른 어떤 신호보다 먼저 팀의 것이다** —
|
|
24
|
+
* 커밋된 설정을 개인 상태로 옮기면 다른 사람의 저장소에서 파일이 사라진다.
|
|
25
|
+
*/
|
|
26
|
+
export declare function judgeAdoption(input: AdoptionInput): Adoption;
|
|
27
|
+
export type MigrationPlan = {
|
|
28
|
+
from: string;
|
|
29
|
+
to: string;
|
|
30
|
+
adoption: Adoption;
|
|
31
|
+
/** 옮길 항목 수. 0이면 옮길 것이 없다. */
|
|
32
|
+
entries: number;
|
|
33
|
+
};
|
|
34
|
+
export type MigrationOutcome = {
|
|
35
|
+
ok: true;
|
|
36
|
+
plan: MigrationPlan;
|
|
37
|
+
verified: string[];
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
reason: 'AMBIGUOUS_ADOPTION';
|
|
41
|
+
adoption: Adoption;
|
|
42
|
+
detail: string;
|
|
43
|
+
} | {
|
|
44
|
+
ok: false;
|
|
45
|
+
reason: 'PROJECT_ADOPTED';
|
|
46
|
+
adoption: Adoption;
|
|
47
|
+
detail: string;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
reason: 'TARGET_EXISTS';
|
|
51
|
+
detail: string;
|
|
52
|
+
} | {
|
|
53
|
+
ok: false;
|
|
54
|
+
reason: 'VERIFY_FAILED';
|
|
55
|
+
detail: string;
|
|
56
|
+
mismatched: string[];
|
|
57
|
+
};
|
|
58
|
+
export type MigrateInput = {
|
|
59
|
+
from: string;
|
|
60
|
+
to: string;
|
|
61
|
+
adoption: Adoption;
|
|
62
|
+
/** 판정을 무시하고 옮긴다. **ambiguous에서만 의미가 있다** — 사람이 확인했다는 뜻이다. */
|
|
63
|
+
force?: boolean;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* 복사하고, 복사된 것이 같은지 확인한다. **원본은 지우지 않는다.**
|
|
67
|
+
*
|
|
68
|
+
* 지우는 것은 사람이 확인한 뒤에 할 일이다. 여기서 지우면 verify가 실패했을 때 되돌릴
|
|
69
|
+
* 것이 없다.
|
|
70
|
+
*/
|
|
71
|
+
export declare function migrate(input: MigrateInput): Promise<MigrationOutcome>;
|
|
72
|
+
/** 사람이 읽는 줄. 무엇을 근거로 그렇게 봤는지가 함께 와야 사람이 뒤집을 수 있다. */
|
|
73
|
+
export declare function adoptionLine(adoption: Adoption): string;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Legacy Migration — 저장소 안에 있던 `.asc/` 를 사용자 소유 공간으로 옮긴다 (C-11 §6).
|
|
2
|
+
//
|
|
3
|
+
// **가장 나쁜 결과는 팀이 채택한 artefact를 개인 상태로 오판해 옮기는 것이다.** 그래서
|
|
4
|
+
// 옮기기 전에 무엇인지부터 판정하고, 모르면 옮기지 않는다:
|
|
5
|
+
//
|
|
6
|
+
// personal legacy 개인이 만든 것 → 옮길 후보
|
|
7
|
+
// project adopted 팀이 채택한 것 → 그대로 둔다
|
|
8
|
+
// ambiguous 판단 불가 → 자동 이동 금지, 사람이 정한다
|
|
9
|
+
//
|
|
10
|
+
// 판정은 **추론이 아니라 증거**로 한다. Git이 추적하고 있으면 팀의 것이고, 추적 제외
|
|
11
|
+
// 목록에 개인용으로 등록돼 있으면 개인 것이다. 둘 다 아니면 모른다 — 그 상태를
|
|
12
|
+
// "아마 개인 것"으로 읽는 순간 남의 팀 설정이 사라진다.
|
|
13
|
+
//
|
|
14
|
+
// 옮기는 순서도 되돌릴 수 있게 잡는다: 복사 → 확인 → 등록 → (원본은 사람이 지운다).
|
|
15
|
+
// verify 전에 원본을 지우지 않는다 (C-11 불변식 ⑮).
|
|
16
|
+
import { cp, readdir, readFile, stat } from 'node:fs/promises';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
/**
|
|
19
|
+
* 이 `.asc/` 가 누구 것인가.
|
|
20
|
+
*
|
|
21
|
+
* 순서가 중요하다. **추적된 파일이 있으면 다른 어떤 신호보다 먼저 팀의 것이다** —
|
|
22
|
+
* 커밋된 설정을 개인 상태로 옮기면 다른 사람의 저장소에서 파일이 사라진다.
|
|
23
|
+
*/
|
|
24
|
+
export function judgeAdoption(input) {
|
|
25
|
+
if (input.trackedAscPaths.length > 0) {
|
|
26
|
+
return {
|
|
27
|
+
kind: 'PROJECT_ADOPTED',
|
|
28
|
+
evidence: `Git이 추적하는 .asc 파일 ${input.trackedAscPaths.length}건 (${input.trackedAscPaths.slice(0, 3).join(', ')})`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (/^\s*\.asc\/?\s*$/m.test(input.gitignoreContent ?? '')) {
|
|
32
|
+
// 팀 파일에 적혀 있다 — 팀이 이 디렉터리를 알고 무시하기로 한 것이다.
|
|
33
|
+
return { kind: 'PROJECT_ADOPTED', evidence: '.gitignore 에 .asc/ 가 선언돼 있다 (팀이 아는 상태)' };
|
|
34
|
+
}
|
|
35
|
+
if (/^\s*\.asc\/?\s*$/m.test(input.excludeContent ?? '')) {
|
|
36
|
+
return { kind: 'PERSONAL_LEGACY', evidence: '.git/info/exclude 에만 등록된 개인 작업 공간' };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
kind: 'AMBIGUOUS',
|
|
40
|
+
evidence: 'Git 추적도 추적 제외 등록도 없다 — 개인 것인지 팀 것인지 판단할 근거가 없다',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 복사하고, 복사된 것이 같은지 확인한다. **원본은 지우지 않는다.**
|
|
45
|
+
*
|
|
46
|
+
* 지우는 것은 사람이 확인한 뒤에 할 일이다. 여기서 지우면 verify가 실패했을 때 되돌릴
|
|
47
|
+
* 것이 없다.
|
|
48
|
+
*/
|
|
49
|
+
export async function migrate(input) {
|
|
50
|
+
if (input.adoption.kind === 'PROJECT_ADOPTED') {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: 'PROJECT_ADOPTED',
|
|
54
|
+
adoption: input.adoption,
|
|
55
|
+
detail: '팀이 채택한 상태다 — 개인 공간으로 옮기지 않는다',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (input.adoption.kind === 'AMBIGUOUS' && !input.force) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
reason: 'AMBIGUOUS_ADOPTION',
|
|
62
|
+
adoption: input.adoption,
|
|
63
|
+
detail: '개인 것인지 팀 것인지 모른다 — 확인한 뒤 --force 로 다시 부르라',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (await exists(input.to)) {
|
|
67
|
+
return { ok: false, reason: 'TARGET_EXISTS', detail: `${input.to} 가 이미 있다 — 덮어쓰지 않는다` };
|
|
68
|
+
}
|
|
69
|
+
const before = await fileMap(input.from);
|
|
70
|
+
await cp(input.from, input.to, { recursive: true });
|
|
71
|
+
// 옮겨진 것이 같은지 본다. 복사만 하고 "됐다"고 말하면 그게 유실의 시작이다.
|
|
72
|
+
const after = await fileMap(input.to);
|
|
73
|
+
const mismatched = [...before.entries()]
|
|
74
|
+
.filter(([rel, content]) => after.get(rel) !== content)
|
|
75
|
+
.map(([rel]) => rel);
|
|
76
|
+
if (mismatched.length > 0) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
reason: 'VERIFY_FAILED',
|
|
80
|
+
detail: `${mismatched.length}건이 원본과 다르다 — 원본은 그대로 두었다`,
|
|
81
|
+
mismatched: mismatched.slice(0, 10),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
plan: { from: input.from, to: input.to, adoption: input.adoption, entries: before.size },
|
|
87
|
+
verified: [...before.keys()].sort(),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function exists(path) {
|
|
91
|
+
try {
|
|
92
|
+
await stat(path);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** 상대 경로 → 내용. 대조는 목록이 아니라 내용으로 한다. */
|
|
100
|
+
async function fileMap(root) {
|
|
101
|
+
const out = new Map();
|
|
102
|
+
const walk = async (dir, prefix) => {
|
|
103
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
104
|
+
const full = join(dir, entry.name);
|
|
105
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
106
|
+
if (entry.isDirectory())
|
|
107
|
+
await walk(full, rel);
|
|
108
|
+
else
|
|
109
|
+
out.set(rel, await readFile(full, 'utf8'));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
await walk(root, '');
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/** 사람이 읽는 줄. 무엇을 근거로 그렇게 봤는지가 함께 와야 사람이 뒤집을 수 있다. */
|
|
116
|
+
export function adoptionLine(adoption) {
|
|
117
|
+
const label = {
|
|
118
|
+
PROJECT_ADOPTED: '팀이 채택한 상태',
|
|
119
|
+
PERSONAL_LEGACY: '개인 작업 공간',
|
|
120
|
+
AMBIGUOUS: '판단 불가',
|
|
121
|
+
}[adoption.kind];
|
|
122
|
+
return `${label} — ${adoption.evidence}`;
|
|
123
|
+
}
|