@agentsmd/core 0.0.0-stage → 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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -2
  3. package/dist/check.d.ts +20 -0
  4. package/dist/check.d.ts.map +1 -0
  5. package/dist/check.js +67 -0
  6. package/dist/check.js.map +1 -0
  7. package/dist/errors.d.ts +7 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +10 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/index.d.ts +9 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +5 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/install.d.ts +19 -0
  16. package/dist/install.d.ts.map +1 -0
  17. package/dist/install.js +33 -0
  18. package/dist/install.js.map +1 -0
  19. package/dist/parse-doc-map.d.ts +9 -0
  20. package/dist/parse-doc-map.d.ts.map +1 -0
  21. package/dist/parse-doc-map.js +73 -0
  22. package/dist/parse-doc-map.js.map +1 -0
  23. package/dist/preset.d.ts +20 -0
  24. package/dist/preset.d.ts.map +1 -0
  25. package/dist/preset.js +2 -0
  26. package/dist/preset.js.map +1 -0
  27. package/dist/resolve-link.d.ts +11 -0
  28. package/dist/resolve-link.d.ts.map +1 -0
  29. package/dist/resolve-link.js +21 -0
  30. package/dist/resolve-link.js.map +1 -0
  31. package/dist/resolve-presets.d.ts +9 -0
  32. package/dist/resolve-presets.d.ts.map +1 -0
  33. package/dist/resolve-presets.js +26 -0
  34. package/dist/resolve-presets.js.map +1 -0
  35. package/dist/rules/broken-link.d.ts +11 -0
  36. package/dist/rules/broken-link.d.ts.map +1 -0
  37. package/dist/rules/broken-link.js +26 -0
  38. package/dist/rules/broken-link.js.map +1 -0
  39. package/dist/rules/claude-import.d.ts +13 -0
  40. package/dist/rules/claude-import.d.ts.map +1 -0
  41. package/dist/rules/claude-import.js +46 -0
  42. package/dist/rules/claude-import.js.map +1 -0
  43. package/dist/rules/unlisted-doc.d.ts +12 -0
  44. package/dist/rules/unlisted-doc.d.ts.map +1 -0
  45. package/dist/rules/unlisted-doc.js +27 -0
  46. package/dist/rules/unlisted-doc.js.map +1 -0
  47. package/dist/types.d.ts +25 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +2 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +38 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 이태경
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,13 @@
1
- # Temporary Holding Version
1
+ # @agentsmd/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [`agentsmd`](https://www.npmjs.com/package/agentsmd) CLI의 검사 로직과 프리셋 설치 로직입니다.
4
+ CLI 없이 코드에서 검사하고 싶을 때 씁니다.
5
+
6
+ ```ts
7
+ import { checkRepository } from "@agentsmd/core";
8
+
9
+ const findings = await checkRepository({ root: process.cwd() });
10
+ // [{ ruleId: 'broken-link', file: 'AGENTS.md', line: 12, message: '링크 대상이 없습니다: docs/api.md' }]
11
+ ```
12
+
13
+ 검사 규칙은 [docs/rules.md](https://github.com/xoruddl/agentsmd-kit/blob/main/docs/rules.md)를 참고하세요.
@@ -0,0 +1,20 @@
1
+ import type { Finding } from "./types.js";
2
+ export interface CheckOptions {
3
+ /** 검사할 저장소의 루트 디렉터리. */
4
+ readonly root: string;
5
+ /** 문서 지도 파일. 루트 기준 상대 경로. 기본값 `AGENTS.md` */
6
+ readonly agentsFile?: string;
7
+ /** 지도에 모두 올라 있어야 하는 문서 디렉터리. 루트 기준 상대 경로. 기본값 `docs` */
8
+ readonly docsDir?: string;
9
+ /** Claude Code용 지침 파일. 루트 기준 상대 경로. 기본값 `CLAUDE.md` */
10
+ readonly claudeFile?: string;
11
+ }
12
+ /**
13
+ * 저장소의 AI 작업 지침을 검사하고, 찾은 문제를 파일·줄 순서로 돌려준다.
14
+ *
15
+ * 규칙들은 파일 시스템을 모르는 순수 함수이고, 파일을 읽는 일은 이 함수가 모두 맡는다.
16
+ *
17
+ * @throws {DocMapNotFoundError} 문서 지도 파일이 없을 때
18
+ */
19
+ export declare function checkRepository(options: CheckOptions): Promise<Finding[]>;
20
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,YAAY;IAC3B,wBAAwB;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6CAA6C;IAC7C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,wDAAwD;IACxD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAgB/E"}
package/dist/check.js ADDED
@@ -0,0 +1,67 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile, readdir } from "node:fs/promises";
3
+ import { join, posix, sep } from "node:path";
4
+ import { DocMapNotFoundError } from "./errors.js";
5
+ import { parseDocMap } from "./parse-doc-map.js";
6
+ import { findBrokenLinks } from "./rules/broken-link.js";
7
+ import { findMissingClaudeImport } from "./rules/claude-import.js";
8
+ import { findUnlistedDocs } from "./rules/unlisted-doc.js";
9
+ /**
10
+ * 저장소의 AI 작업 지침을 검사하고, 찾은 문제를 파일·줄 순서로 돌려준다.
11
+ *
12
+ * 규칙들은 파일 시스템을 모르는 순수 함수이고, 파일을 읽는 일은 이 함수가 모두 맡는다.
13
+ *
14
+ * @throws {DocMapNotFoundError} 문서 지도 파일이 없을 때
15
+ */
16
+ export async function checkRepository(options) {
17
+ const { root, agentsFile = "AGENTS.md", docsDir = "docs", claudeFile = "CLAUDE.md" } = options;
18
+ const agentsContent = await readOptional(join(root, agentsFile));
19
+ if (agentsContent === undefined) {
20
+ throw new DocMapNotFoundError(agentsFile);
21
+ }
22
+ const map = parseDocMap(agentsFile, agentsContent);
23
+ const exists = (repoPath) => existsSync(join(root, repoPath));
24
+ const findings = [
25
+ ...findBrokenLinks(map, exists),
26
+ ...findUnlistedDocs([map], await listMarkdownFiles(root, docsDir)),
27
+ ...findMissingClaudeImport(claudeFile, await readOptional(join(root, claudeFile)), agentsFile),
28
+ ];
29
+ return findings.sort(byLocation);
30
+ }
31
+ async function readOptional(path) {
32
+ try {
33
+ return await readFile(path, "utf8");
34
+ }
35
+ catch (error) {
36
+ if (isNotFound(error)) {
37
+ return undefined;
38
+ }
39
+ throw error;
40
+ }
41
+ }
42
+ /** 문서 디렉터리 아래의 .md 파일을 루트 기준 posix 경로로 모은다. 디렉터리가 없으면 빈 목록이다. */
43
+ async function listMarkdownFiles(root, docsDir) {
44
+ try {
45
+ const entries = await readdir(join(root, docsDir), { recursive: true, withFileTypes: true });
46
+ return entries
47
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
48
+ .map((entry) => toRepoPath(root, join(entry.parentPath, entry.name)))
49
+ .sort();
50
+ }
51
+ catch (error) {
52
+ if (isNotFound(error)) {
53
+ return [];
54
+ }
55
+ throw error;
56
+ }
57
+ }
58
+ function toRepoPath(root, absolutePath) {
59
+ return posix.normalize(absolutePath.slice(join(root, sep).length).split(sep).join("/"));
60
+ }
61
+ function isNotFound(error) {
62
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
63
+ }
64
+ function byLocation(a, b) {
65
+ return a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0);
66
+ }
67
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /** 검사의 출발점인 문서 지도 파일이 없을 때. 규칙 위반이 아니라 검사를 시작할 수 없는 상황이다. */
2
+ export declare class DocMapNotFoundError extends Error {
3
+ readonly path: string;
4
+ readonly name = "DocMapNotFoundError";
5
+ constructor(path: string);
6
+ }
7
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,qBAAa,mBAAoB,SAAQ,KAAK;IAGhC,QAAQ,CAAC,IAAI,EAAE,MAAM;IAFjC,SAAkB,IAAI,yBAAyB;gBAE1B,IAAI,EAAE,MAAM;CAGlC"}
package/dist/errors.js ADDED
@@ -0,0 +1,10 @@
1
+ /** 검사의 출발점인 문서 지도 파일이 없을 때. 규칙 위반이 아니라 검사를 시작할 수 없는 상황이다. */
2
+ export class DocMapNotFoundError extends Error {
3
+ path;
4
+ name = "DocMapNotFoundError";
5
+ constructor(path) {
6
+ super(`문서 지도를 찾을 수 없습니다: ${path}`);
7
+ this.path = path;
8
+ }
9
+ }
10
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAGvB;IAFH,IAAI,GAAG,qBAAqB,CAAC;IAE/C,YAAqB,IAAY;QAC/B,KAAK,CAAC,qBAAqB,IAAI,EAAE,CAAC,CAAC;QADhB,SAAI,GAAJ,IAAI,CAAQ;IAEjC,CAAC;CACF"}
@@ -0,0 +1,9 @@
1
+ export { checkRepository } from "./check.js";
2
+ export type { CheckOptions } from "./check.js";
3
+ export { DocMapNotFoundError } from "./errors.js";
4
+ export { installPreset } from "./install.js";
5
+ export type { InstallOptions, InstallResult } from "./install.js";
6
+ export type { Preset } from "./preset.js";
7
+ export { resolvePresets } from "./resolve-presets.js";
8
+ export type { DocLink, DocMap, Finding, RuleId } from "./types.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClE,YAAY,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { checkRepository } from "./check.js";
2
+ export { DocMapNotFoundError } from "./errors.js";
3
+ export { installPreset } from "./install.js";
4
+ export { resolvePresets } from "./resolve-presets.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAG7C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { Preset } from "./preset.js";
2
+ export interface InstallOptions {
3
+ /** 이미 있는 파일도 덮어쓴다. 기본값 false. */
4
+ readonly force?: boolean;
5
+ }
6
+ export interface InstallResult {
7
+ /** 새로 만들었거나 덮어쓴 파일. 대상 디렉터리 기준 posix 경로. */
8
+ readonly written: readonly string[];
9
+ /** 이미 있어서 건드리지 않은 파일. 대상 디렉터리 기준 posix 경로. */
10
+ readonly skipped: readonly string[];
11
+ }
12
+ /**
13
+ * 프리셋 파일을 대상 디렉터리에 복사한다.
14
+ *
15
+ * 기존 저장소에 적용할 때 사람이 다듬어 둔 지침을 지우지 않도록, 기본으로는 이미 있는 파일을 건너뛴다.
16
+ * 파일 권한도 함께 복사되므로 Git 훅의 실행 권한이 유지된다.
17
+ */
18
+ export declare function installPreset(preset: Preset, targetDir: string, options?: InstallOptions): Promise<InstallResult>;
19
+ //# sourceMappingURL=install.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,WAAW,cAAc;IAC7B,iCAAiC;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,aAAa,CAAC,CAgBxB"}
@@ -0,0 +1,33 @@
1
+ import { copyFile, mkdir, readdir } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, join, relative, sep } from "node:path";
4
+ /**
5
+ * 프리셋 파일을 대상 디렉터리에 복사한다.
6
+ *
7
+ * 기존 저장소에 적용할 때 사람이 다듬어 둔 지침을 지우지 않도록, 기본으로는 이미 있는 파일을 건너뛴다.
8
+ * 파일 권한도 함께 복사되므로 Git 훅의 실행 권한이 유지된다.
9
+ */
10
+ export async function installPreset(preset, targetDir, options = {}) {
11
+ const written = [];
12
+ const skipped = [];
13
+ for (const file of await listFiles(preset.filesDir)) {
14
+ const destination = join(targetDir, file);
15
+ if (options.force !== true && existsSync(destination)) {
16
+ skipped.push(file);
17
+ continue;
18
+ }
19
+ await mkdir(dirname(destination), { recursive: true });
20
+ await copyFile(join(preset.filesDir, file), destination);
21
+ written.push(file);
22
+ }
23
+ return { written, skipped };
24
+ }
25
+ /** 디렉터리 아래 모든 파일을 posix 상대 경로로, 정렬해서 돌려준다. */
26
+ async function listFiles(dir) {
27
+ const entries = await readdir(dir, { recursive: true, withFileTypes: true });
28
+ return entries
29
+ .filter((entry) => entry.isFile())
30
+ .map((entry) => relative(dir, join(entry.parentPath, entry.name)).split(sep).join("/"))
31
+ .sort();
32
+ }
33
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAezD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAc,EACd,SAAiB,EACjB,UAA0B,EAAE;IAE5B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,KAAK,MAAM,IAAI,IAAI,MAAM,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAC1C,IAAI,OAAO,CAAC,KAAK,KAAK,IAAI,IAAI,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YACtD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,SAAS;QACX,CAAC;QACD,MAAM,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,MAAM,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,WAAW,CAAC,CAAC;QACzD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC9B,CAAC;AAED,8CAA8C;AAC9C,KAAK,UAAU,SAAS,CAAC,GAAW;IAClC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7E,OAAO,OAAO;SACX,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;SACjC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;SACtF,IAAI,EAAE,CAAC;AACZ,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { DocMap } from "./types.js";
2
+ /**
3
+ * 문서 지도(AGENTS.md 등)에서 저장소 안의 파일을 가리키는 링크를 모은다.
4
+ *
5
+ * 코드 블록과 인라인 코드 안의 링크는 예시일 수 있으므로 세지 않는다.
6
+ * 경로가 실제로 있는지, 저장소 밖을 가리키는지는 여기서 판단하지 않고 규칙에 맡긴다.
7
+ */
8
+ export declare function parseDocMap(file: string, markdown: string): DocMap;
9
+ //# sourceMappingURL=parse-doc-map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-doc-map.d.ts","sourceRoot":"","sources":["../src/parse-doc-map.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,MAAM,EAAE,MAAM,YAAY,CAAC;AAYlD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAwBlE"}
@@ -0,0 +1,73 @@
1
+ // [텍스트](경로) 또는 ![대체 텍스트](경로). 뒤에 "제목"이 올 수 있다.
2
+ // 공백이 든 경로는 <...>로 감싸야 하므로 꺾쇠 형식(1번 그룹)과 일반 형식(2번 그룹)을 나눠 받는다.
3
+ const INLINE_LINK = /!?\[(?:[^\]\\]|\\.)*\]\(\s*(?:<([^>\n]+)>|([^)\s]+))(?:\s+"[^"]*")?\s*\)/g;
4
+ // [이름]: 경로 (참조형 링크 정의)
5
+ const REFERENCE_DEFINITION = /^\s{0,3}\[[^\]]+\]:\s*<?(\S+?)>?(?:\s|$)/;
6
+ // http:, mailto: 처럼 스킴이 붙은 주소는 저장소 안의 파일이 아니다.
7
+ const URL_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
8
+ const FENCE = /^\s{0,3}(`{3,}|~{3,})/;
9
+ const INLINE_CODE = /`+[^`]*`+/g;
10
+ /**
11
+ * 문서 지도(AGENTS.md 등)에서 저장소 안의 파일을 가리키는 링크를 모은다.
12
+ *
13
+ * 코드 블록과 인라인 코드 안의 링크는 예시일 수 있으므로 세지 않는다.
14
+ * 경로가 실제로 있는지, 저장소 밖을 가리키는지는 여기서 판단하지 않고 규칙에 맡긴다.
15
+ */
16
+ export function parseDocMap(file, markdown) {
17
+ const links = [];
18
+ let openFence;
19
+ markdown.split(/\r?\n/).forEach((rawLine, index) => {
20
+ const fence = FENCE.exec(rawLine)?.[1];
21
+ if (fence !== undefined) {
22
+ openFence = nextFenceState(openFence, fence);
23
+ return;
24
+ }
25
+ if (openFence !== undefined) {
26
+ return;
27
+ }
28
+ const line = index + 1;
29
+ for (const target of extractTargets(rawLine.replace(INLINE_CODE, ""))) {
30
+ const normalized = normalizeTarget(target);
31
+ if (normalized !== undefined) {
32
+ links.push({ target: normalized, line });
33
+ }
34
+ }
35
+ });
36
+ return { file, links };
37
+ }
38
+ /** 같은 문자로 같은 길이 이상인 펜스가 와야 코드 블록이 닫힌다 (CommonMark 규칙). */
39
+ function nextFenceState(openFence, fence) {
40
+ if (openFence === undefined) {
41
+ return fence;
42
+ }
43
+ const closes = fence.startsWith(openFence.charAt(0)) && fence.length >= openFence.length;
44
+ return closes ? undefined : openFence;
45
+ }
46
+ function extractTargets(line) {
47
+ const definition = REFERENCE_DEFINITION.exec(line)?.[1];
48
+ if (definition !== undefined) {
49
+ return [definition];
50
+ }
51
+ return Array.from(line.matchAll(INLINE_LINK), (match) => match[1] ?? match[2] ?? "");
52
+ }
53
+ /** 앵커와 쿼리를 떼고, 외부 주소나 같은 문서 안의 앵커면 undefined를 돌려준다. */
54
+ function normalizeTarget(target) {
55
+ if (target === "" || target.startsWith("#") || URL_SCHEME.test(target)) {
56
+ return undefined;
57
+ }
58
+ const path = target.split(/[#?]/, 1)[0] ?? "";
59
+ if (path === "") {
60
+ return undefined;
61
+ }
62
+ return safeDecode(path);
63
+ }
64
+ /** `%20` 같은 인코딩을 풀되, 잘못된 인코딩이면 적힌 그대로 둔다. */
65
+ function safeDecode(path) {
66
+ try {
67
+ return decodeURI(path);
68
+ }
69
+ catch {
70
+ return path;
71
+ }
72
+ }
73
+ //# sourceMappingURL=parse-doc-map.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-doc-map.js","sourceRoot":"","sources":["../src/parse-doc-map.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `init`으로 설치할 수 있는 지침 파일 묶음.
3
+ *
4
+ * 프리셋 패키지는 이 형태의 객체 하나를 내보낸다. 파일 내용은 `filesDir` 아래에 설치될 경로 그대로 둔다.
5
+ */
6
+ export interface Preset {
7
+ /** `--preset` 옵션에 쓰는 이름. 예: `java` */
8
+ readonly name: string;
9
+ /** 도움말에 보여 줄 한 줄 설명. */
10
+ readonly description: string;
11
+ /** 설치할 파일들이 있는 디렉터리의 절대 경로. */
12
+ readonly filesDir: string;
13
+ /** 프리셋에 Git 훅이 있으면 그 디렉터리. 설치 대상 기준 상대 경로. `core.hooksPath`로 연결한다. */
14
+ readonly hooksPath?: string;
15
+ /** 파일을 설치한 뒤 사람이 직접 해야 하는 일. 설치 결과 끝에 안내한다. */
16
+ readonly nextSteps?: readonly string[];
17
+ /** 이 프리셋보다 먼저 설치해야 하는 프리셋. 예: 언어 프리셋이 공통 Git 프리셋을 요구한다. */
18
+ readonly requires?: readonly Preset[];
19
+ }
20
+ //# sourceMappingURL=preset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preset.d.ts","sourceRoot":"","sources":["../src/preset.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,MAAM;IACrB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wBAAwB;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,+BAA+B;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC"}
package/dist/preset.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=preset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preset.js","sourceRoot":"","sources":["../src/preset.ts"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 문서 안의 링크를 저장소 루트 기준 경로로 바꾼다.
3
+ *
4
+ * 링크는 그 링크가 적힌 문서의 위치를 기준으로 풀린다. `/`로 시작하면 GitHub처럼 저장소 루트 기준으로 본다.
5
+ * 저장소 밖을 가리키면(예: `../menu-pick-frontend`) 이 저장소만으로는 확인할 수 없으므로 undefined를 돌려준다.
6
+ *
7
+ * @param fromFile 링크가 적힌 문서. 저장소 루트 기준 상대 경로.
8
+ * @param target 링크에 적힌 경로.
9
+ */
10
+ export declare function resolveLink(fromFile: string, target: string): string | undefined;
11
+ //# sourceMappingURL=resolve-link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-link.d.ts","sourceRoot":"","sources":["../src/resolve-link.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUhF"}
@@ -0,0 +1,21 @@
1
+ import { posix } from "node:path";
2
+ /**
3
+ * 문서 안의 링크를 저장소 루트 기준 경로로 바꾼다.
4
+ *
5
+ * 링크는 그 링크가 적힌 문서의 위치를 기준으로 풀린다. `/`로 시작하면 GitHub처럼 저장소 루트 기준으로 본다.
6
+ * 저장소 밖을 가리키면(예: `../menu-pick-frontend`) 이 저장소만으로는 확인할 수 없으므로 undefined를 돌려준다.
7
+ *
8
+ * @param fromFile 링크가 적힌 문서. 저장소 루트 기준 상대 경로.
9
+ * @param target 링크에 적힌 경로.
10
+ */
11
+ export function resolveLink(fromFile, target) {
12
+ const joined = target.startsWith("/")
13
+ ? target.slice(1)
14
+ : posix.join(posix.dirname(fromFile), target);
15
+ const normalized = posix.normalize(joined).replace(/\/+$/, "");
16
+ if (normalized === ".." || normalized.startsWith("../")) {
17
+ return undefined;
18
+ }
19
+ return normalized === "" ? "." : normalized;
20
+ }
21
+ //# sourceMappingURL=resolve-link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-link.js","sourceRoot":"","sources":["../src/resolve-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAElC;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,QAAgB,EAAE,MAAc;IAC1D,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC;QACnC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACjB,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;IAChD,MAAM,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAE/D,IAAI,UAAU,KAAK,IAAI,IAAI,UAAU,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QACxD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { Preset } from "./preset.js";
2
+ /**
3
+ * 프리셋과 그 프리셋이 요구하는 프리셋을 설치 순서대로 펼친다.
4
+ *
5
+ * 요구되는 프리셋이 먼저 오고, 여러 경로로 요구돼도 한 번만 들어간다 (이름 기준).
6
+ * 순환 요구는 설치 순서를 정할 수 없으므로 오류로 알린다.
7
+ */
8
+ export declare function resolvePresets(preset: Preset): Preset[];
9
+ //# sourceMappingURL=resolve-presets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-presets.d.ts","sourceRoot":"","sources":["../src/resolve-presets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAoBvD"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 프리셋과 그 프리셋이 요구하는 프리셋을 설치 순서대로 펼친다.
3
+ *
4
+ * 요구되는 프리셋이 먼저 오고, 여러 경로로 요구돼도 한 번만 들어간다 (이름 기준).
5
+ * 순환 요구는 설치 순서를 정할 수 없으므로 오류로 알린다.
6
+ */
7
+ export function resolvePresets(preset) {
8
+ const ordered = [];
9
+ const done = new Set();
10
+ const visit = (current, path) => {
11
+ if (path.includes(current.name)) {
12
+ throw new Error(`프리셋 요구 관계가 순환합니다: ${[...path, current.name].join(" → ")}`);
13
+ }
14
+ if (done.has(current.name)) {
15
+ return;
16
+ }
17
+ for (const required of current.requires ?? []) {
18
+ visit(required, [...path, current.name]);
19
+ }
20
+ done.add(current.name);
21
+ ordered.push(current);
22
+ };
23
+ visit(preset, []);
24
+ return ordered;
25
+ }
26
+ //# sourceMappingURL=resolve-presets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-presets.js","sourceRoot":"","sources":["../src/resolve-presets.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAE/B,MAAM,KAAK,GAAG,CAAC,OAAe,EAAE,IAAuB,EAAQ,EAAE;QAC/D,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC9E,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,OAAO;QACT,CAAC;QACD,KAAK,MAAM,QAAQ,IAAI,OAAO,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;YAC9C,KAAK,CAAC,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3C,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC,CAAC;IAEF,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAClB,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { DocMap, Finding } from "../types.js";
2
+ /**
3
+ * 문서 지도가 가리키는 파일·디렉터리가 실제로 있는지 검사한다.
4
+ *
5
+ * 문서를 옮기거나 지운 뒤 지도를 고치지 않으면, 에이전트는 없는 문서를 찾다가 추측으로 작업하게 된다.
6
+ * 저장소 밖을 가리키는 링크는 확인할 수 없으므로 건너뛴다.
7
+ *
8
+ * @param exists 저장소 루트 기준 경로가 있는지 알려 주는 함수. 파일 시스템 접근은 호출하는 쪽이 맡는다.
9
+ */
10
+ export declare function findBrokenLinks(map: DocMap, exists: (repoPath: string) => boolean): Finding[];
11
+ //# sourceMappingURL=broken-link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broken-link.d.ts","sourceRoot":"","sources":["../../src/rules/broken-link.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEnD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,EAAE,CAe7F"}
@@ -0,0 +1,26 @@
1
+ import { resolveLink } from "../resolve-link.js";
2
+ /**
3
+ * 문서 지도가 가리키는 파일·디렉터리가 실제로 있는지 검사한다.
4
+ *
5
+ * 문서를 옮기거나 지운 뒤 지도를 고치지 않으면, 에이전트는 없는 문서를 찾다가 추측으로 작업하게 된다.
6
+ * 저장소 밖을 가리키는 링크는 확인할 수 없으므로 건너뛴다.
7
+ *
8
+ * @param exists 저장소 루트 기준 경로가 있는지 알려 주는 함수. 파일 시스템 접근은 호출하는 쪽이 맡는다.
9
+ */
10
+ export function findBrokenLinks(map, exists) {
11
+ return map.links.flatMap((link) => {
12
+ const repoPath = resolveLink(map.file, link.target);
13
+ if (repoPath === undefined || exists(repoPath)) {
14
+ return [];
15
+ }
16
+ return [
17
+ {
18
+ ruleId: "broken-link",
19
+ file: map.file,
20
+ line: link.line,
21
+ message: `링크 대상이 없습니다: ${link.target}`,
22
+ },
23
+ ];
24
+ });
25
+ }
26
+ //# sourceMappingURL=broken-link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broken-link.js","sourceRoot":"","sources":["../../src/rules/broken-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGjD;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,MAAqC;IAChF,OAAO,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QAChC,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACpD,IAAI,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/C,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,OAAO;YACL;gBACE,MAAM,EAAE,aAAa;gBACrB,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,OAAO,EAAE,gBAAgB,IAAI,CAAC,MAAM,EAAE;aACvC;SACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { Finding } from "../types.js";
2
+ /**
3
+ * CLAUDE.md가 AGENTS.md를 `@` 문법으로 불러오는지 검사한다.
4
+ *
5
+ * Claude Code는 CLAUDE.md를 읽으므로, 지침의 원본을 AGENTS.md 하나로 두려면 CLAUDE.md가 그 파일을 불러와야 한다.
6
+ * 그렇지 않으면 두 파일의 지침이 따로 자라거나, Claude Code만 지침을 읽지 못한다.
7
+ *
8
+ * @param claudeFile CLAUDE.md의 저장소 루트 기준 경로.
9
+ * @param content CLAUDE.md 내용. 파일이 없으면 undefined이고, 이때는 검사할 대상이 없다.
10
+ * @param agentsFile 불러와야 하는 문서 지도의 저장소 루트 기준 경로.
11
+ */
12
+ export declare function findMissingClaudeImport(claudeFile: string, content: string | undefined, agentsFile: string): Finding[];
13
+ //# sourceMappingURL=claude-import.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-import.d.ts","sourceRoot":"","sources":["../../src/rules/claude-import.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,UAAU,EAAE,MAAM,GACjB,OAAO,EAAE,CAeX"}
@@ -0,0 +1,46 @@
1
+ import { posix } from "node:path";
2
+ /**
3
+ * CLAUDE.md가 AGENTS.md를 `@` 문법으로 불러오는지 검사한다.
4
+ *
5
+ * Claude Code는 CLAUDE.md를 읽으므로, 지침의 원본을 AGENTS.md 하나로 두려면 CLAUDE.md가 그 파일을 불러와야 한다.
6
+ * 그렇지 않으면 두 파일의 지침이 따로 자라거나, Claude Code만 지침을 읽지 못한다.
7
+ *
8
+ * @param claudeFile CLAUDE.md의 저장소 루트 기준 경로.
9
+ * @param content CLAUDE.md 내용. 파일이 없으면 undefined이고, 이때는 검사할 대상이 없다.
10
+ * @param agentsFile 불러와야 하는 문서 지도의 저장소 루트 기준 경로.
11
+ */
12
+ export function findMissingClaudeImport(claudeFile, content, agentsFile) {
13
+ if (content === undefined) {
14
+ return [];
15
+ }
16
+ const importPath = posix.relative(posix.dirname(claudeFile), agentsFile);
17
+ if (importsFile(content, importPath)) {
18
+ return [];
19
+ }
20
+ return [
21
+ {
22
+ ruleId: "claude-import",
23
+ file: claudeFile,
24
+ message: `@${importPath} 로 문서 지도를 불러오지 않습니다`,
25
+ },
26
+ ];
27
+ }
28
+ /** `@AGENTS.md`, `@./AGENTS.md` 모두 인정한다. 코드 블록 안의 예시는 인정하지 않는다. */
29
+ function importsFile(content, importPath) {
30
+ let inFence = false;
31
+ for (const line of content.split(/\r?\n/)) {
32
+ if (/^\s{0,3}(`{3,}|~{3,})/.test(line)) {
33
+ inFence = !inFence;
34
+ continue;
35
+ }
36
+ if (inFence) {
37
+ continue;
38
+ }
39
+ const imports = line.match(/(?:^|\s)@(\S+)/g) ?? [];
40
+ if (imports.some((token) => posix.normalize(token.trim().slice(1)) === importPath)) {
41
+ return true;
42
+ }
43
+ }
44
+ return false;
45
+ }
46
+ //# sourceMappingURL=claude-import.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-import.js","sourceRoot":"","sources":["../../src/rules/claude-import.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAGlC;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CACrC,UAAkB,EAClB,OAA2B,EAC3B,UAAkB;IAElB,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,UAAU,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;IACzE,IAAI,WAAW,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC;QACrC,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO;QACL;YACE,MAAM,EAAE,eAAe;YACvB,IAAI,EAAE,UAAU;YAChB,OAAO,EAAE,IAAI,UAAU,qBAAqB;SAC7C;KACF,CAAC;AACJ,CAAC;AAED,mEAAmE;AACnE,SAAS,WAAW,CAAC,OAAe,EAAE,UAAkB;IACtD,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1C,IAAI,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACvC,OAAO,GAAG,CAAC,OAAO,CAAC;YACnB,SAAS;QACX,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACZ,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC;QACpD,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,UAAU,CAAC,EAAE,CAAC;YACnF,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { DocMap, Finding } from "../types.js";
2
+ /**
3
+ * 문서 디렉터리에 있지만 어떤 문서 지도에서도 안내하지 않는 문서를 찾는다.
4
+ *
5
+ * 에이전트는 지도에 없는 문서를 읽을 이유가 없으므로, 새 문서를 만들고 지도를 고치지 않으면
6
+ * 그 문서의 규칙은 없는 것과 같다. 지도가 디렉터리를 가리키면 그 아래 문서는 모두 안내된 것으로 본다.
7
+ *
8
+ * @param maps 문서 지도들. 하나라도 가리키면 안내된 것으로 본다.
9
+ * @param docFiles 검사할 문서 목록. 저장소 루트 기준 상대 경로.
10
+ */
11
+ export declare function findUnlistedDocs(maps: readonly DocMap[], docFiles: readonly string[]): Finding[];
12
+ //# sourceMappingURL=unlisted-doc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unlisted-doc.d.ts","sourceRoot":"","sources":["../../src/rules/unlisted-doc.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEnD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,EAAE,CAahG"}
@@ -0,0 +1,27 @@
1
+ import { resolveLink } from "../resolve-link.js";
2
+ /**
3
+ * 문서 디렉터리에 있지만 어떤 문서 지도에서도 안내하지 않는 문서를 찾는다.
4
+ *
5
+ * 에이전트는 지도에 없는 문서를 읽을 이유가 없으므로, 새 문서를 만들고 지도를 고치지 않으면
6
+ * 그 문서의 규칙은 없는 것과 같다. 지도가 디렉터리를 가리키면 그 아래 문서는 모두 안내된 것으로 본다.
7
+ *
8
+ * @param maps 문서 지도들. 하나라도 가리키면 안내된 것으로 본다.
9
+ * @param docFiles 검사할 문서 목록. 저장소 루트 기준 상대 경로.
10
+ */
11
+ export function findUnlistedDocs(maps, docFiles) {
12
+ const listed = new Set(maps.flatMap((map) => map.links.flatMap((link) => resolveLink(map.file, link.target) ?? [])));
13
+ const mapNames = maps.map((map) => map.file).join(", ");
14
+ return docFiles
15
+ .filter((docFile) => !isListed(docFile, listed))
16
+ .map((docFile) => ({
17
+ ruleId: "unlisted-doc",
18
+ file: docFile,
19
+ message: `문서 지도(${mapNames})에서 안내하지 않는 문서입니다`,
20
+ }));
21
+ }
22
+ /** 파일 자체나 그 파일을 담은 상위 디렉터리 중 하나가 지도에 있으면 안내된 것이다. */
23
+ function isListed(docFile, listed) {
24
+ const parts = docFile.split("/");
25
+ return parts.some((_, index) => listed.has(parts.slice(0, index + 1).join("/")));
26
+ }
27
+ //# sourceMappingURL=unlisted-doc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unlisted-doc.js","sourceRoot":"","sources":["../../src/rules/unlisted-doc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGjD;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAuB,EAAE,QAA2B;IACnF,MAAM,MAAM,GAAG,IAAI,GAAG,CACpB,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAC7F,CAAC;IACF,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAExD,OAAO,QAAQ;SACZ,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;SAC/C,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACjB,MAAM,EAAE,cAAc;QACtB,IAAI,EAAE,OAAO;QACb,OAAO,EAAE,SAAS,QAAQ,mBAAmB;KAC9C,CAAC,CAAC,CAAC;AACR,CAAC;AAED,qDAAqD;AACrD,SAAS,QAAQ,CAAC,OAAe,EAAE,MAA2B;IAC5D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACnF,CAAC"}
@@ -0,0 +1,25 @@
1
+ /** AGENTS.md 안에서 찾은 상대 경로 링크 하나. */
2
+ export interface DocLink {
3
+ /** 링크에 적힌 경로. `#` 뒤의 앵커는 뺀 값이다. */
4
+ readonly target: string;
5
+ /** 링크가 있는 줄 번호 (1부터 시작). */
6
+ readonly line: number;
7
+ }
8
+ /** 검사 대상이 되는 문서 지도 파일 하나. */
9
+ export interface DocMap {
10
+ /** 저장소 루트 기준 상대 경로. 예: `AGENTS.md` */
11
+ readonly file: string;
12
+ readonly links: readonly DocLink[];
13
+ }
14
+ /** 규칙 식별자. 출력과 문서에서 이 이름으로 규칙을 가리킨다. */
15
+ export type RuleId = "broken-link" | "unlisted-doc" | "claude-import";
16
+ /** 규칙 하나가 찾은 문제. */
17
+ export interface Finding {
18
+ readonly ruleId: RuleId;
19
+ /** 문제가 있는 파일. 저장소 루트 기준 상대 경로. */
20
+ readonly file: string;
21
+ /** 줄을 특정할 수 있을 때만 있다. */
22
+ readonly line?: number;
23
+ readonly message: string;
24
+ }
25
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,MAAM,WAAW,OAAO;IACtB,mCAAmC;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4BAA4B;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,6BAA6B;AAC7B,MAAM,WAAW,MAAM;IACrB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,CAAC;CACpC;AAED,wCAAwC;AACxC,MAAM,MAAM,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,eAAe,CAAC;AAEtE,oBAAoB;AACpB,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kCAAkC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yBAAyB;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@agentsmd/core",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "AGENTS.md 문서 지도를 읽고 검사하는 규칙 모음",
5
+ "keywords": [
6
+ "agents.md",
7
+ "ai-agent",
8
+ "lint",
9
+ "documentation"
10
+ ],
11
+ "homepage": "https://github.com/xoruddl/agentsmd-kit#readme",
12
+ "bugs": "https://github.com/xoruddl/agentsmd-kit/issues",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/xoruddl/agentsmd-kit.git",
16
+ "directory": "packages/core"
17
+ },
18
+ "license": "MIT",
19
+ "author": "이태경 (https://github.com/xoruddl)",
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "engines": {
31
+ "node": ">=22"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.build.json",
38
+ "typecheck": "tsc -p tsconfig.json",
39
+ "test": "vitest run"
40
+ }
6
41
  }