phasegate 0.91.0 → 0.108.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/CHANGELOG.md +255 -0
- package/README.ja.md +17 -7
- package/README.md +26 -4
- package/docs/ADR/ADR-015-architecture-preset.md +183 -0
- package/docs/guide/cli-reference.md +108 -0
- package/docs/guide/codex-integration.md +7 -2
- package/docs/guide/installation.md +10 -2
- package/docs/guide/preset-selection.md +170 -0
- package/docs/guide/quick-vs-full-mode.md +3 -3
- package/docs/guide/retrofit-adoption.md +19 -2
- package/docs/guide/skills-overview.md +1 -1
- package/package.json +7 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -100
- package/scripts/harness/agent-integration/domain/ports/phase-gate-query-port.ts +3 -3
- package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +26 -30
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +6 -2
- package/scripts/harness/biome-ast-engine/application/dto/analyze-import-graph-input.ts +3 -0
- package/scripts/harness/biome-ast-engine/application/dto/resolve-enabled-rules-output.ts +2 -0
- package/scripts/harness/biome-ast-engine/application/mappers/resolve-enabled-rules-output-mapper.ts +4 -1
- package/scripts/harness/biome-ast-engine/application/usecases/analyze-import-graph-usecase.ts +4 -1
- package/scripts/harness/biome-ast-engine/application/usecases/execute-lint-usecase.ts +2 -0
- package/scripts/harness/biome-ast-engine/application/usecases/resolve-enabled-rules-usecase.ts +31 -3
- package/scripts/harness/biome-ast-engine/composition-root.ts +10 -2
- package/scripts/harness/biome-ast-engine/domain/ports/rule-config-provider-port.ts +16 -0
- package/scripts/harness/biome-ast-engine/domain/ports/source-module-analyzer-port.ts +5 -1
- package/scripts/harness/biome-ast-engine/domain/services/lint-runner.ts +5 -1
- package/scripts/harness/biome-ast-engine/domain/value-objects/architecture-spec.ts +38 -0
- package/scripts/harness/biome-ast-engine/domain/value-objects/layer-boundary.ts +7 -8
- package/scripts/harness/biome-ast-engine/domain/value-objects/layer-name.ts +15 -23
- package/scripts/harness/biome-ast-engine/domain/value-objects/source-module-snapshot.ts +11 -4
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/harness-config-provider-adapter.ts +29 -3
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +22 -15
- package/scripts/harness/biome-ast-engine/infrastructure/mappers/source-module-snapshot-mapper.ts +26 -17
- package/scripts/harness/config-foundation/application/dto/resolved-config-output.ts +1 -0
- package/scripts/harness/config-foundation/application/usecases/load-resolved-config-use-case.ts +34 -2
- package/scripts/harness/config-foundation/application/usecases/migrate-schema-use-case.ts +89 -0
- package/scripts/harness/config-foundation/composition-root.ts +8 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +6 -0
- package/scripts/harness/config-foundation/domain/services/architecture-resolution-service.ts +257 -0
- package/scripts/harness/config-foundation/domain/value-objects/architecture-config.ts +66 -0
- package/scripts/harness/config-foundation/domain/value-objects/architecture-preset-catalog.ts +75 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +546 -0
- package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +18 -6
- package/scripts/harness/config-foundation/presentation/cli/migrate-schema-command-handler.ts +83 -0
- package/scripts/harness/integrations/pre-commit.ts +211 -52
- package/scripts/harness/main.ts +460 -340
- package/scripts/harness/phase-dependency-model/domain/ports/story-reflection-file-system-port.ts +2 -4
- package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +54 -17
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +154 -36
- package/scripts/harness/setup/skill-deployer.ts +140 -102
- package/scripts/harness/skill-quality/domain/errors/skill-quality-error.ts +24 -23
- package/scripts/harness/skill-quality/domain/value-objects/commit-message.ts +23 -7
- package/scripts/harness/traceability-model/application/usecases/apply-work-item-migration-usecase.ts +52 -0
- package/scripts/harness/traceability-model/application/usecases/plan-work-item-migration-usecase.ts +29 -0
- package/scripts/harness/traceability-model/application/usecases/validate-design-story-annotations-usecase.ts +83 -18
- package/scripts/harness/traceability-model/composition-root.ts +48 -30
- package/scripts/harness/traceability-model/domain/ports/design-document-port.ts +9 -15
- package/scripts/harness/traceability-model/domain/ports/work-item-migration-apply-port.ts +11 -0
- package/scripts/harness/traceability-model/domain/ports/work-item-migration-source-port.ts +9 -0
- package/scripts/harness/traceability-model/domain/services/work-item-migration-planner.ts +162 -0
- package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +57 -0
- package/scripts/harness/traceability-model/domain/value-objects/work-item-migration-candidate.ts +47 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-apply-gateway.ts +110 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-source-gateway.ts +182 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/markdown-design-document-gateway.ts +29 -43
- package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +136 -0
- package/scripts/harness/traceability-model/presentation/cli/migrate-work-items-command-handler.ts +186 -0
- package/skills/quick-implementor/SKILL.md +17 -1
- package/templates/.husky/commit-msg +1 -0
package/scripts/harness/phase-dependency-model/domain/ports/story-reflection-file-system-port.ts
CHANGED
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
export interface StoryReflectionFileSystemPort {
|
|
5
5
|
listStoryDirectories(unitId: string): Promise<readonly string[]>;
|
|
6
|
+
storyAffectsUnit(storyId: string, unitId: string): Promise<boolean>;
|
|
6
7
|
fileExists(path: string): Promise<boolean>;
|
|
7
8
|
/** product 文書内に @story-id {storyId} アノテーションが含まれているか */
|
|
8
|
-
fileContainsStoryAnnotation(
|
|
9
|
-
productPath: string,
|
|
10
|
-
storyId: string,
|
|
11
|
-
): Promise<boolean>;
|
|
9
|
+
fileContainsStoryAnnotation(productPath: string, storyId: string): Promise<boolean>;
|
|
12
10
|
}
|
|
@@ -3,20 +3,22 @@
|
|
|
3
3
|
* @unit phase-dependency-model
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
import type {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
import type { StoryReflectionFileSystemPort } from "../ports/story-reflection-file-system-port.js";
|
|
7
|
+
import type { StoryReflectionConfig } from "../values/story-reflection-config.js";
|
|
8
|
+
import type { StoryReflectionMapping } from "../values/story-reflection-mapping.js";
|
|
9
|
+
import { StoryReflectionResult, type StoryReflectionViolation } from "../values/story-reflection-result.js";
|
|
10
|
+
|
|
11
|
+
const CROSS_WORK_ITEM_PATTERN = /^WI-\d+$/;
|
|
12
|
+
|
|
13
|
+
interface ResolvedInceptionPath {
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly isCrossWorkItem: boolean;
|
|
16
|
+
}
|
|
12
17
|
|
|
13
18
|
export class StoryReflectionChecker {
|
|
14
19
|
constructor(private readonly fsPort: StoryReflectionFileSystemPort) {}
|
|
15
20
|
|
|
16
|
-
async check(
|
|
17
|
-
unitId: string,
|
|
18
|
-
config: StoryReflectionConfig,
|
|
19
|
-
): Promise<StoryReflectionResult> {
|
|
21
|
+
async check(unitId: string, config: StoryReflectionConfig): Promise<StoryReflectionResult> {
|
|
20
22
|
if (!config.enabled) {
|
|
21
23
|
return StoryReflectionResult.pass();
|
|
22
24
|
}
|
|
@@ -32,23 +34,28 @@ export class StoryReflectionChecker {
|
|
|
32
34
|
|
|
33
35
|
for (const storyId of storyIds) {
|
|
34
36
|
for (const mapping of config.mappings) {
|
|
35
|
-
const {
|
|
36
|
-
|
|
37
|
+
const { product: productPath } = mapping.resolve({ unitId, storyId });
|
|
38
|
+
const resolvedInception = await this.resolveInceptionPath({
|
|
39
|
+
mapping,
|
|
40
|
+
unitId,
|
|
41
|
+
storyId,
|
|
42
|
+
});
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
if (resolvedInception === null) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
39
47
|
|
|
40
|
-
if (!
|
|
48
|
+
if (resolvedInception.isCrossWorkItem && !(await this.fsPort.storyAffectsUnit(storyId, unitId))) {
|
|
41
49
|
continue;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
const hasAnnotation =
|
|
45
|
-
await this.fsPort.fileContainsStoryAnnotation(productPath, storyId);
|
|
52
|
+
const hasAnnotation = await this.fsPort.fileContainsStoryAnnotation(productPath, storyId);
|
|
46
53
|
|
|
47
54
|
if (!hasAnnotation) {
|
|
48
55
|
const violation: StoryReflectionViolation = {
|
|
49
56
|
storyId,
|
|
50
57
|
mapping,
|
|
51
|
-
inceptionPath,
|
|
58
|
+
inceptionPath: resolvedInception.path,
|
|
52
59
|
productPath,
|
|
53
60
|
};
|
|
54
61
|
|
|
@@ -63,4 +70,34 @@ export class StoryReflectionChecker {
|
|
|
63
70
|
|
|
64
71
|
return StoryReflectionResult.create({ violations, warnings });
|
|
65
72
|
}
|
|
73
|
+
|
|
74
|
+
private async resolveInceptionPath(input: {
|
|
75
|
+
readonly mapping: StoryReflectionMapping;
|
|
76
|
+
readonly unitId: string;
|
|
77
|
+
readonly storyId: string;
|
|
78
|
+
}): Promise<ResolvedInceptionPath | null> {
|
|
79
|
+
const normalPath = input.mapping.resolve({
|
|
80
|
+
unitId: input.unitId,
|
|
81
|
+
storyId: input.storyId,
|
|
82
|
+
}).inception;
|
|
83
|
+
|
|
84
|
+
if (await this.fsPort.fileExists(normalPath)) {
|
|
85
|
+
return { path: normalPath, isCrossWorkItem: false };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!CROSS_WORK_ITEM_PATTERN.test(input.storyId)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const crossPath = input.mapping.resolve({
|
|
93
|
+
unitId: "_cross",
|
|
94
|
+
storyId: input.storyId,
|
|
95
|
+
}).inception;
|
|
96
|
+
|
|
97
|
+
if (await this.fsPort.fileExists(crossPath)) {
|
|
98
|
+
return { path: crossPath, isCrossWorkItem: true };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
66
103
|
}
|
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
* @unit phase-dependency-model
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import
|
|
6
|
+
import type { Dirent } from "node:fs";
|
|
7
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import type { StoryReflectionFileSystemPort } from "../../domain/ports/story-reflection-file-system-port.js";
|
|
10
|
+
|
|
11
|
+
const CROSS_WORK_ITEM_PATTERN = /^WI-\d+$/;
|
|
12
|
+
const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
13
|
+
const AFFECTS_INLINE_PATTERN = /^affects:\s*\[([^\]]*)\]\s*$/m;
|
|
14
|
+
const LEGACY_ID_PATTERN = /^legacy_id:\s*([A-Z][\w]+-\d+)\s*$/m;
|
|
9
15
|
|
|
10
16
|
export interface FileSystemStoryReflectionAdapterDeps {
|
|
11
17
|
readonly rootDir: string;
|
|
@@ -15,39 +21,67 @@ export interface FileSystemStoryReflectionAdapterDeps {
|
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
23
|
* ファイルシステム上の docs/inception ディレクトリ列挙と
|
|
18
|
-
* product 文書内 `@story-id`
|
|
24
|
+
* product 文書内 `@story-id` / `@issue-id` / `@work-item-id` アノテーション
|
|
25
|
+
* 検索を実装するアダプタ。
|
|
19
26
|
*
|
|
20
27
|
* 存在しないパス・読めないファイルは true/false に正規化し例外を投げない
|
|
21
28
|
* ことで、上位 (StoryReflectionChecker) の純粋性を保つ。
|
|
29
|
+
*
|
|
30
|
+
* ISSUE-026 / H02-04: `@issue-id` / `@work-item-id` も `@story-id` と同等に
|
|
31
|
+
* 認識する(WI 一本化への段階移行のため、既存 `@story-id` と併存)。
|
|
22
32
|
*/
|
|
23
|
-
export class FileSystemStoryReflectionAdapter
|
|
24
|
-
implements StoryReflectionFileSystemPort
|
|
25
|
-
{
|
|
33
|
+
export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSystemPort {
|
|
26
34
|
private readonly rootDir: string;
|
|
27
35
|
private readonly inceptionRoot: string;
|
|
28
36
|
|
|
29
37
|
constructor(deps: FileSystemStoryReflectionAdapterDeps) {
|
|
30
38
|
this.rootDir = deps.rootDir;
|
|
31
|
-
this.inceptionRoot = deps.inceptionRoot ??
|
|
39
|
+
this.inceptionRoot = deps.inceptionRoot ?? "docs/inception";
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
async listStoryDirectories(unitId: string): Promise<readonly string[]> {
|
|
35
|
-
const
|
|
43
|
+
const ids = new Set<string>();
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
for (const id of await this.listUnitWorkItemDirectories(unitId)) {
|
|
46
|
+
ids.add(id);
|
|
47
|
+
}
|
|
48
|
+
for (const id of await this.listCrossWorkItemDirectories()) {
|
|
49
|
+
ids.add(id);
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
return [...ids].sort();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async listUnitWorkItemDirectories(unitId: string): Promise<readonly string[]> {
|
|
56
|
+
const unitDir = path.join(this.rootDir, this.inceptionRoot, unitId);
|
|
57
|
+
const entries = await this.readDirectories(unitDir);
|
|
58
|
+
|
|
59
|
+
return entries
|
|
60
|
+
.filter((entry) => entry.isDirectory())
|
|
61
|
+
.map((entry) => entry.name)
|
|
62
|
+
.filter((name) => !name.startsWith("_") && !name.startsWith("."))
|
|
63
|
+
.sort();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async listCrossWorkItemDirectories(): Promise<readonly string[]> {
|
|
67
|
+
const crossDir = path.join(this.rootDir, this.inceptionRoot, "_cross");
|
|
68
|
+
const entries = await this.readDirectories(crossDir);
|
|
69
|
+
|
|
44
70
|
return entries
|
|
45
71
|
.filter((entry) => entry.isDirectory())
|
|
46
72
|
.map((entry) => entry.name)
|
|
47
|
-
.filter((name) =>
|
|
73
|
+
.filter((name) => CROSS_WORK_ITEM_PATTERN.test(name))
|
|
48
74
|
.sort();
|
|
49
75
|
}
|
|
50
76
|
|
|
77
|
+
private async readDirectories(dir: string): Promise<readonly Dirent[]> {
|
|
78
|
+
try {
|
|
79
|
+
return await readdir(dir, { withFileTypes: true });
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
51
85
|
async fileExists(relativePath: string): Promise<boolean> {
|
|
52
86
|
const absolutePath = path.join(this.rootDir, relativePath);
|
|
53
87
|
try {
|
|
@@ -58,37 +92,121 @@ export class FileSystemStoryReflectionAdapter
|
|
|
58
92
|
}
|
|
59
93
|
}
|
|
60
94
|
|
|
61
|
-
async
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
95
|
+
async storyAffectsUnit(storyId: string, unitId: string): Promise<boolean> {
|
|
96
|
+
if (!CROSS_WORK_ITEM_PATTERN.test(storyId)) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let content: string;
|
|
101
|
+
try {
|
|
102
|
+
content = await readFile(
|
|
103
|
+
path.join(this.rootDir, this.inceptionRoot, "_cross", storyId, "description.md"),
|
|
104
|
+
"utf8",
|
|
105
|
+
);
|
|
106
|
+
} catch {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const frontmatter = FRONTMATTER_PATTERN.exec(content)?.[1];
|
|
111
|
+
if (frontmatter === undefined) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const affects = AFFECTS_INLINE_PATTERN.exec(frontmatter)?.[1]
|
|
116
|
+
.split(",")
|
|
117
|
+
.map((value) => value.trim())
|
|
118
|
+
.filter((value) => value.length > 0);
|
|
119
|
+
|
|
120
|
+
if (affects === undefined) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return affects.includes(unitId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async fileContainsStoryAnnotation(productPath: string, storyId: string): Promise<boolean> {
|
|
65
128
|
const absolutePath = path.join(this.rootDir, productPath);
|
|
66
129
|
|
|
67
130
|
let content: string;
|
|
68
131
|
try {
|
|
69
|
-
content = await readFile(absolutePath,
|
|
132
|
+
content = await readFile(absolutePath, "utf8");
|
|
70
133
|
} catch {
|
|
71
134
|
return false;
|
|
72
135
|
}
|
|
73
136
|
|
|
74
|
-
// `@story-id`
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
137
|
+
// `@story-id` / `@issue-id` / `@work-item-id` 直後にカンマ/空白区切りで
|
|
138
|
+
// 列挙される ID を抽出。
|
|
139
|
+
// 例: `@story-id US-001`, `@work-item-id WI-001, WI-002`,
|
|
140
|
+
// `<!-- @issue-id ISSUE-026 -->`
|
|
141
|
+
// capture[1] がアノテーション種別(未使用・将来拡張用)、capture[2] が ID 本体。
|
|
142
|
+
// 行末または `-->` まで(ID 自体の `-` を許容するため `-->` を明示終端に使う)。
|
|
143
|
+
const annotationIds = this.extractAnnotationIds(content);
|
|
144
|
+
if (annotationIds.includes(storyId)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const legacyId = await this.readLegacyId(storyId);
|
|
149
|
+
return legacyId !== null && annotationIds.includes(legacyId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private extractAnnotationIds(content: string): readonly string[] {
|
|
153
|
+
const ids: string[] = [];
|
|
154
|
+
|
|
155
|
+
const annotationPattern = /@(story-id|issue-id|work-item-id)[ \t]+([^\n\r]+)/g;
|
|
156
|
+
let match = annotationPattern.exec(content);
|
|
157
|
+
while (match !== null) {
|
|
81
158
|
// `-->` 以降の HTML コメント閉じ記号を取り除く
|
|
82
|
-
const raw = match[
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
159
|
+
const raw = match[2].replace(/-->.*$/, "");
|
|
160
|
+
ids.push(
|
|
161
|
+
...raw
|
|
162
|
+
.split(/[\s,]+/)
|
|
163
|
+
.map((s) => s.trim())
|
|
164
|
+
.filter((s) => s.length > 0),
|
|
165
|
+
);
|
|
166
|
+
match = annotationPattern.exec(content);
|
|
90
167
|
}
|
|
91
168
|
|
|
92
|
-
return
|
|
169
|
+
return ids;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private async readLegacyId(storyId: string): Promise<string | null> {
|
|
173
|
+
if (!CROSS_WORK_ITEM_PATTERN.test(storyId)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const candidate of await this.listDescriptionCandidates(storyId)) {
|
|
178
|
+
const legacyId = await this.tryReadLegacyId(candidate);
|
|
179
|
+
if (legacyId !== null) return legacyId;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async listDescriptionCandidates(storyId: string): Promise<readonly string[]> {
|
|
186
|
+
const inceptionDir = path.join(this.rootDir, this.inceptionRoot);
|
|
187
|
+
const candidates: string[] = [path.join(inceptionDir, "_cross", storyId, "description.md")];
|
|
188
|
+
|
|
189
|
+
for (const entry of await this.readDirectories(inceptionDir)) {
|
|
190
|
+
if (!entry.isDirectory()) continue;
|
|
191
|
+
const name = entry.name;
|
|
192
|
+
if (name.startsWith("_") || name.startsWith(".") || name === "issues") continue;
|
|
193
|
+
candidates.push(path.join(inceptionDir, name, storyId, "description.md"));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return candidates;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private async tryReadLegacyId(absolutePath: string): Promise<string | null> {
|
|
200
|
+
let content: string;
|
|
201
|
+
try {
|
|
202
|
+
content = await readFile(absolutePath, "utf8");
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const frontmatter = FRONTMATTER_PATTERN.exec(content)?.[1];
|
|
208
|
+
if (frontmatter === undefined) return null;
|
|
209
|
+
|
|
210
|
+
return LEGACY_ID_PATTERN.exec(frontmatter)?.[1] ?? null;
|
|
93
211
|
}
|
|
94
212
|
}
|