crumbs-spec 0.0.0-stage → 0.2.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 +21 -0
- package/README.ko.md +397 -0
- package/README.md +398 -2
- package/dist/src/check.js +72 -0
- package/dist/src/check.js.map +1 -0
- package/dist/src/cli.js +195 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.js +151 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/dates.js +22 -0
- package/dist/src/dates.js.map +1 -0
- package/dist/src/graph.js +241 -0
- package/dist/src/graph.js.map +1 -0
- package/dist/src/headings.js +55 -0
- package/dist/src/headings.js.map +1 -0
- package/dist/src/init.js +114 -0
- package/dist/src/init.js.map +1 -0
- package/dist/src/log.js +65 -0
- package/dist/src/log.js.map +1 -0
- package/dist/src/markdown.js +323 -0
- package/dist/src/markdown.js.map +1 -0
- package/dist/src/messages.js +203 -0
- package/dist/src/messages.js.map +1 -0
- package/dist/src/model.js +4 -0
- package/dist/src/model.js.map +1 -0
- package/dist/src/report.js +14 -0
- package/dist/src/report.js.map +1 -0
- package/dist/src/rules/broken-anchor.js +23 -0
- package/dist/src/rules/broken-anchor.js.map +1 -0
- package/dist/src/rules/broken-link.js +21 -0
- package/dist/src/rules/broken-link.js.map +1 -0
- package/dist/src/rules/duplicate-policy.js +47 -0
- package/dist/src/rules/duplicate-policy.js.map +1 -0
- package/dist/src/rules/log-body.js +23 -0
- package/dist/src/rules/log-body.js.map +1 -0
- package/dist/src/rules/log-format.js +34 -0
- package/dist/src/rules/log-format.js.map +1 -0
- package/dist/src/rules/log-order.js +23 -0
- package/dist/src/rules/log-order.js.map +1 -0
- package/dist/src/rules/missing-backlink.js +42 -0
- package/dist/src/rules/missing-backlink.js.map +1 -0
- package/dist/src/rules/missing-code-path.js +16 -0
- package/dist/src/rules/missing-code-path.js.map +1 -0
- package/dist/src/rules/missing-flow.js +17 -0
- package/dist/src/rules/missing-flow.js.map +1 -0
- package/dist/src/rules/missing-section.js +31 -0
- package/dist/src/rules/missing-section.js.map +1 -0
- package/dist/src/rules/open-question.js +13 -0
- package/dist/src/rules/open-question.js.map +1 -0
- package/dist/src/rules/section-order.js +46 -0
- package/dist/src/rules/section-order.js.map +1 -0
- package/dist/src/rules/unreachable.js +27 -0
- package/dist/src/rules/unreachable.js.map +1 -0
- package/dist/src/rules/unterminated-comment.js +13 -0
- package/dist/src/rules/unterminated-comment.js.map +1 -0
- package/dist/src/skills.js +27 -0
- package/dist/src/skills.js.map +1 -0
- package/dist/src/templates/agents.js +54 -0
- package/dist/src/templates/agents.js.map +1 -0
- package/dist/src/templates/en.js +75 -0
- package/dist/src/templates/en.js.map +1 -0
- package/dist/src/templates/ko.js +75 -0
- package/dist/src/templates/ko.js.map +1 -0
- package/dist/src/templates/skills-en.js +260 -0
- package/dist/src/templates/skills-en.js.map +1 -0
- package/dist/src/templates/skills-ko.js +265 -0
- package/dist/src/templates/skills-ko.js.map +1 -0
- package/dist/src/version.js +7 -0
- package/dist/src/version.js.map +1 -0
- package/package.json +50 -4
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { runCheck } from './check.js';
|
|
6
|
+
import { ConfigError, loadRepoConfig } from './config.js';
|
|
7
|
+
import { isLang } from './headings.js';
|
|
8
|
+
import { runInit } from './init.js';
|
|
9
|
+
import { addLogEntry, assertLogDocument, hasHeadingLine, isLogKind, normalizeLogBody } from './log.js';
|
|
10
|
+
import { MESSAGES } from './messages.js';
|
|
11
|
+
import { formatJson, formatText } from './report.js';
|
|
12
|
+
// 사용법 오류. 메시지 뒤에 한 줄 사용법을 덧붙여 stderr에 낸다(종료 코드 2). lang은 그 줄의 언어다.
|
|
13
|
+
class UsageError extends Error {
|
|
14
|
+
lang;
|
|
15
|
+
constructor(message, lang) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.lang = lang;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const OPTIONS = {
|
|
21
|
+
root: { type: 'string' },
|
|
22
|
+
help: { type: 'boolean', short: 'h' },
|
|
23
|
+
version: { type: 'boolean', short: 'v' },
|
|
24
|
+
lang: { type: 'string' },
|
|
25
|
+
'spec-dir': { type: 'string' },
|
|
26
|
+
format: { type: 'string' },
|
|
27
|
+
config: { type: 'string' },
|
|
28
|
+
kind: { type: 'string' },
|
|
29
|
+
body: { type: 'string' },
|
|
30
|
+
skills: { type: 'string' },
|
|
31
|
+
};
|
|
32
|
+
// 명령마다 받는 옵션(6.4.4 표). --root와 --lang은 모든 명령이 받으므로 여기에 없다.
|
|
33
|
+
const SCOPED_OPTIONS = ['spec-dir', 'format', 'config', 'kind', 'body', 'skills'];
|
|
34
|
+
const INIT_OPTIONS = ['spec-dir', 'skills'];
|
|
35
|
+
const CHECK_OPTIONS = ['format', 'config'];
|
|
36
|
+
const LOG_ADD_OPTIONS = ['config', 'kind', 'body'];
|
|
37
|
+
function readVersion() {
|
|
38
|
+
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
39
|
+
return pkg.version;
|
|
40
|
+
}
|
|
41
|
+
// 설정 파일을 읽기 전의 오류 언어. --lang 값이 ko이면 ko이고, 그 밖에는 en이다.
|
|
42
|
+
function preLang(argv) {
|
|
43
|
+
for (let i = 0; i < argv.length; i++) {
|
|
44
|
+
if (argv[i] === '--')
|
|
45
|
+
break;
|
|
46
|
+
if (argv[i] === '--lang')
|
|
47
|
+
return argv[i + 1] === 'ko' ? 'ko' : 'en';
|
|
48
|
+
if (argv[i].startsWith('--lang='))
|
|
49
|
+
return argv[i].slice('--lang='.length) === 'ko' ? 'ko' : 'en';
|
|
50
|
+
}
|
|
51
|
+
return 'en';
|
|
52
|
+
}
|
|
53
|
+
// parseArgs 오류를 사용법 오류로 바꾼다. 옵션 이름은 오류 메시지에서 꺼낸다.
|
|
54
|
+
function toUsageError(error, lang) {
|
|
55
|
+
if (!(error instanceof Error))
|
|
56
|
+
return new UsageError(String(error), lang);
|
|
57
|
+
const code = error.code;
|
|
58
|
+
const name = /(--?[A-Za-z][\w-]*)/.exec(error.message)?.[1] ?? error.message;
|
|
59
|
+
const m = MESSAGES[lang];
|
|
60
|
+
if (code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION')
|
|
61
|
+
return new UsageError(m.unknownOption(name), lang);
|
|
62
|
+
if (code === 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE')
|
|
63
|
+
return new UsageError(m.optionValue(name), lang);
|
|
64
|
+
return error;
|
|
65
|
+
}
|
|
66
|
+
function parse(argv, lang) {
|
|
67
|
+
try {
|
|
68
|
+
return parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw toUsageError(error, lang);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// 명령이 받지 않는 옵션을 "알 수 없는 옵션"으로 거부한다.
|
|
75
|
+
function rejectForeignOptions(values, allowed, lang) {
|
|
76
|
+
for (const name of SCOPED_OPTIONS) {
|
|
77
|
+
if (values[name] !== undefined && !allowed.includes(name)) {
|
|
78
|
+
throw new UsageError(MESSAGES[lang].unknownOption(`--${name}`), lang);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// --lang 값을 검사한다. 옵션이 없으면 undefined다.
|
|
83
|
+
function langOption(value, lang) {
|
|
84
|
+
if (value === undefined)
|
|
85
|
+
return undefined;
|
|
86
|
+
if (isLang(value))
|
|
87
|
+
return value;
|
|
88
|
+
throw new UsageError(MESSAGES[lang].optionValue('--lang'), lang);
|
|
89
|
+
}
|
|
90
|
+
// --skills 값을 검사한다. 옵션이 없으면 undefined다.
|
|
91
|
+
function skillsOption(value, lang) {
|
|
92
|
+
if (value === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
if (value === 'claude' || value === 'codex' || value === 'all')
|
|
95
|
+
return value;
|
|
96
|
+
throw new UsageError(MESSAGES[lang].optionValue('--skills'), lang);
|
|
97
|
+
}
|
|
98
|
+
// log add(6.4). 검증은 6.4.2 표의 순서대로 하고, 처음 걸린 조건만 보고한다.
|
|
99
|
+
// 설정을 읽기 전 조건은 명령줄의 언어로, 설정을 읽은 뒤 조건은 설정의 lang으로 낸다.
|
|
100
|
+
function logAdd(root, values, args, lang) {
|
|
101
|
+
const m = MESSAGES[lang];
|
|
102
|
+
if (values.kind === undefined)
|
|
103
|
+
throw new UsageError(m.optionRequired('--kind'), lang);
|
|
104
|
+
const kind = values.kind;
|
|
105
|
+
if (!isLogKind(kind))
|
|
106
|
+
throw new UsageError(m.optionValue('--kind'), lang);
|
|
107
|
+
if (args.length === 0)
|
|
108
|
+
throw new UsageError(m.titleMissing, lang);
|
|
109
|
+
if (args.length > 1)
|
|
110
|
+
throw new UsageError(m.titleCount, lang);
|
|
111
|
+
const [rawTitle] = args;
|
|
112
|
+
if (rawTitle.trim() === '')
|
|
113
|
+
throw new UsageError(m.titleEmpty, lang);
|
|
114
|
+
if (/[\r\n]/.test(rawTitle))
|
|
115
|
+
throw new UsageError(m.titleLineBreak, lang);
|
|
116
|
+
const body = values.body === undefined ? undefined : normalizeLogBody(values.body);
|
|
117
|
+
if (body !== undefined && hasHeadingLine(body))
|
|
118
|
+
throw new UsageError(m.bodyHeading, lang);
|
|
119
|
+
if (rawTitle.includes('<!--') || (body ?? '').includes('<!--'))
|
|
120
|
+
throw new UsageError(m.commentForbidden, lang);
|
|
121
|
+
const config = loadRepoConfig(root, values.config, lang);
|
|
122
|
+
assertLogDocument(root, config);
|
|
123
|
+
rejectForeignOptions(values, LOG_ADD_OPTIONS, config.lang);
|
|
124
|
+
process.stdout.write(`${addLogEntry(root, config, kind, rawTitle.trim(), body)}\n`);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
function run(argv) {
|
|
128
|
+
const lang = preLang(argv);
|
|
129
|
+
const m = MESSAGES[lang];
|
|
130
|
+
const { values, positionals } = parse(argv, lang);
|
|
131
|
+
const specLang = langOption(values.lang, lang);
|
|
132
|
+
if (values.help) {
|
|
133
|
+
process.stdout.write(m.help);
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
if (values.version) {
|
|
137
|
+
process.stdout.write(`${readVersion()}\n`);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
if (positionals.length === 0)
|
|
141
|
+
throw new UsageError(m.noCommand, lang);
|
|
142
|
+
const [command, ...rest] = positionals;
|
|
143
|
+
if (command === 'log') {
|
|
144
|
+
if (rest[0] === undefined)
|
|
145
|
+
throw new UsageError(m.noSubcommand, lang);
|
|
146
|
+
if (rest[0] !== 'add')
|
|
147
|
+
throw new UsageError(m.unknownCommand(`log ${rest[0]}`), lang);
|
|
148
|
+
}
|
|
149
|
+
else if (command !== 'init' && command !== 'check') {
|
|
150
|
+
throw new UsageError(m.unknownCommand(command), lang);
|
|
151
|
+
}
|
|
152
|
+
else if (rest.length > 0) {
|
|
153
|
+
throw new UsageError(m.unknownCommand(rest[0]), lang);
|
|
154
|
+
}
|
|
155
|
+
if (values.root === '')
|
|
156
|
+
throw new UsageError(m.optionValue('--root'), lang);
|
|
157
|
+
const root = resolve(values.root ?? '.');
|
|
158
|
+
if (command === 'log')
|
|
159
|
+
return logAdd(root, values, rest.slice(1), lang);
|
|
160
|
+
if (command === 'init') {
|
|
161
|
+
rejectForeignOptions(values, INIT_OPTIONS, lang);
|
|
162
|
+
const skills = skillsOption(values.skills, lang);
|
|
163
|
+
process.stdout.write(`${runInit(root, values['spec-dir'], skills, specLang).join('\n')}\n`);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
rejectForeignOptions(values, CHECK_OPTIONS, lang);
|
|
167
|
+
const format = values.format ?? 'text';
|
|
168
|
+
if (format !== 'text' && format !== 'json')
|
|
169
|
+
throw new UsageError(m.optionValue('--format'), lang);
|
|
170
|
+
const config = loadRepoConfig(root, values.config, lang);
|
|
171
|
+
const report = runCheck(root, config);
|
|
172
|
+
process.stdout.write(format === 'json' ? formatJson(report) : formatText(report, config.lang));
|
|
173
|
+
return report.ok ? 0 : 1;
|
|
174
|
+
}
|
|
175
|
+
function main() {
|
|
176
|
+
let code;
|
|
177
|
+
try {
|
|
178
|
+
code = run(process.argv.slice(2));
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error instanceof UsageError) {
|
|
182
|
+
process.stderr.write(`crumbs: ${error.message}\n${MESSAGES[error.lang].usage}\n`);
|
|
183
|
+
}
|
|
184
|
+
else if (error instanceof ConfigError) {
|
|
185
|
+
process.stderr.write(`${error.message}\n`);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
process.stderr.write(`crumbs: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
189
|
+
}
|
|
190
|
+
code = 2;
|
|
191
|
+
}
|
|
192
|
+
process.exitCode = code;
|
|
193
|
+
}
|
|
194
|
+
main();
|
|
195
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, posix, resolve } from 'node:path';
|
|
3
|
+
import { VOCAB_KEYS, isLang } from './headings.js';
|
|
4
|
+
import { MESSAGES } from './messages.js';
|
|
5
|
+
// 설정 오류. 메시지는 사용자에게 그대로 보여 준다(stderr, 종료 코드 2).
|
|
6
|
+
export class ConfigError extends Error {
|
|
7
|
+
}
|
|
8
|
+
const TOP_LEVEL_KEYS = ['version', 'specDir', 'index', 'overview', 'log', 'lang', 'headings', 'exclude', 'codeRoot', 'rawDir'];
|
|
9
|
+
// specDir/rawDir를 저장소 루트 기준 정규화 경로로 만든다. 빈 값과 "."은 "."이 된다.
|
|
10
|
+
function normRel(path) {
|
|
11
|
+
return posix.normalize(path).replace(/\/+$/, '') || '.';
|
|
12
|
+
}
|
|
13
|
+
// 저장소 루트 밖을 가리키는 값(.., 절대 경로, 드라이브 문자)인지 본다.
|
|
14
|
+
function isOutsideRoot(rel) {
|
|
15
|
+
return rel === '..' || rel.startsWith('../') || posix.isAbsolute(rel) || /^[A-Za-z]:/.test(rel);
|
|
16
|
+
}
|
|
17
|
+
// exclude 항목을 스펙 루트 기준 정규화 경로로 만든다. 끝의 "/"는 디렉터리 항목이라는 뜻이므로 남긴다.
|
|
18
|
+
function normExclude(m, entry) {
|
|
19
|
+
const path = posix.normalize(entry).replace(/^(\.\/)+/, '');
|
|
20
|
+
if (path === '' || path === '.' || isOutsideRoot(path))
|
|
21
|
+
fail(m, 'exclude', m.reasonRelativePath);
|
|
22
|
+
return path;
|
|
23
|
+
}
|
|
24
|
+
export function defaultConfig(specDir, lang = 'en') {
|
|
25
|
+
return {
|
|
26
|
+
version: 1,
|
|
27
|
+
specDir: specDir ?? 'docs/specs',
|
|
28
|
+
index: 'index.md',
|
|
29
|
+
overview: 'overview.md',
|
|
30
|
+
log: 'log.md',
|
|
31
|
+
lang,
|
|
32
|
+
headings: {},
|
|
33
|
+
exclude: ['templates/'],
|
|
34
|
+
codeRoot: '.',
|
|
35
|
+
rawDir: 'docs/raw',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// 설정 값 오류. 메시지 언어는 validate가 정한다.
|
|
39
|
+
function fail(m, key, reason) {
|
|
40
|
+
throw new ConfigError(m.invalid(key, reason));
|
|
41
|
+
}
|
|
42
|
+
function isPlainObject(value) {
|
|
43
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
// 설정 내용의 오류 메시지 언어. 값이 en·ko인 lang을 쓰고, 그 밖이면 호출자가 준 언어(--lang 또는 en)를 쓴다.
|
|
46
|
+
function messageLang(raw, fallback) {
|
|
47
|
+
if (isPlainObject(raw) && typeof raw.lang === 'string' && isLang(raw.lang))
|
|
48
|
+
return raw.lang;
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
function optString(m, obj, key, fallback) {
|
|
52
|
+
const value = obj[key];
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
return fallback;
|
|
55
|
+
if (typeof value !== 'string')
|
|
56
|
+
fail(m, key, m.reasonString);
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function validate(raw, fallback) {
|
|
60
|
+
const m = MESSAGES[messageLang(raw, fallback)];
|
|
61
|
+
if (!isPlainObject(raw))
|
|
62
|
+
throw new ConfigError(m.configNotObject);
|
|
63
|
+
for (const key of Object.keys(raw)) {
|
|
64
|
+
if (!TOP_LEVEL_KEYS.includes(key))
|
|
65
|
+
throw new ConfigError(m.unknownKey(key));
|
|
66
|
+
}
|
|
67
|
+
const version = raw.version;
|
|
68
|
+
if (version === undefined)
|
|
69
|
+
fail(m, 'version', m.reasonOne);
|
|
70
|
+
if (typeof version !== 'number')
|
|
71
|
+
fail(m, 'version', m.reasonNumber);
|
|
72
|
+
if (version !== 1)
|
|
73
|
+
fail(m, 'version', m.reasonOne);
|
|
74
|
+
const specDir = normRel(optString(m, raw, 'specDir', 'docs/specs'));
|
|
75
|
+
if (specDir === '.')
|
|
76
|
+
fail(m, 'specDir', m.reasonDot);
|
|
77
|
+
if (isOutsideRoot(specDir))
|
|
78
|
+
fail(m, 'specDir', m.reasonInsideRoot);
|
|
79
|
+
const index = optString(m, raw, 'index', 'index.md');
|
|
80
|
+
const overview = optString(m, raw, 'overview', 'overview.md');
|
|
81
|
+
const log = optString(m, raw, 'log', 'log.md');
|
|
82
|
+
const lang = optString(m, raw, 'lang', 'en');
|
|
83
|
+
if (!isLang(lang))
|
|
84
|
+
throw new ConfigError(m.langInvalid);
|
|
85
|
+
const headings = {};
|
|
86
|
+
if (raw.headings !== undefined) {
|
|
87
|
+
const overrides = raw.headings;
|
|
88
|
+
if (!isPlainObject(overrides))
|
|
89
|
+
fail(m, 'headings', m.reasonObject);
|
|
90
|
+
for (const [name, text] of Object.entries(overrides)) {
|
|
91
|
+
if (!VOCAB_KEYS.includes(name))
|
|
92
|
+
fail(m, 'headings', m.reasonUnknownHeading(name));
|
|
93
|
+
if (typeof text !== 'string')
|
|
94
|
+
fail(m, `headings.${name}`, m.reasonString);
|
|
95
|
+
headings[name] = text;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
let exclude = ['templates/'];
|
|
99
|
+
if (raw.exclude !== undefined) {
|
|
100
|
+
const value = raw.exclude;
|
|
101
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {
|
|
102
|
+
fail(m, 'exclude', m.reasonStringArray);
|
|
103
|
+
}
|
|
104
|
+
exclude = value.map((entry) => normExclude(m, entry));
|
|
105
|
+
}
|
|
106
|
+
const codeRoot = optString(m, raw, 'codeRoot', '.');
|
|
107
|
+
const rawDir = normRel(optString(m, raw, 'rawDir', 'docs/raw'));
|
|
108
|
+
if (rawDir === '.')
|
|
109
|
+
fail(m, 'rawDir', m.reasonDot);
|
|
110
|
+
if (isOutsideRoot(rawDir))
|
|
111
|
+
fail(m, 'rawDir', m.reasonInsideRoot);
|
|
112
|
+
if (rawDir === specDir || rawDir.startsWith(`${specDir}/`))
|
|
113
|
+
fail(m, 'rawDir', m.reasonInsideSpec);
|
|
114
|
+
return {
|
|
115
|
+
version: 1,
|
|
116
|
+
specDir,
|
|
117
|
+
index,
|
|
118
|
+
overview,
|
|
119
|
+
log,
|
|
120
|
+
lang,
|
|
121
|
+
headings,
|
|
122
|
+
exclude,
|
|
123
|
+
codeRoot,
|
|
124
|
+
rawDir,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// fallback은 설정 내용이 lang으로 언어를 정하지 못할 때 쓰는 언어다(--lang 값 또는 en).
|
|
128
|
+
export function parseConfig(text, fallback = 'en') {
|
|
129
|
+
let raw;
|
|
130
|
+
try {
|
|
131
|
+
raw = JSON.parse(text);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
throw new ConfigError(`crumbs.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
135
|
+
}
|
|
136
|
+
return validate(raw, fallback);
|
|
137
|
+
}
|
|
138
|
+
// missingLabel은 파일이 없을 때 메시지 앞에 붙는 위치 표시다(기본 설정이면 저장소 루트).
|
|
139
|
+
export function loadConfig(configPath, missingLabel, fallback = 'en') {
|
|
140
|
+
if (!existsSync(configPath)) {
|
|
141
|
+
throw new ConfigError(MESSAGES[fallback].fileMissing(missingLabel));
|
|
142
|
+
}
|
|
143
|
+
return parseConfig(readFileSync(configPath, 'utf8'), fallback);
|
|
144
|
+
}
|
|
145
|
+
// 저장소 설정을 읽는다. configPath가 없으면 저장소 루트의 crumbs.json이다(check와 log add가 함께 쓴다).
|
|
146
|
+
export function loadRepoConfig(root, configPath, fallback = 'en') {
|
|
147
|
+
return configPath === undefined
|
|
148
|
+
? loadConfig(join(root, 'crumbs.json'), root, fallback)
|
|
149
|
+
: loadConfig(resolve(configPath), resolve(configPath), fallback);
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// 날짜 계산. init과 log add가 함께 쓴다.
|
|
2
|
+
// 로컬 시간대 기준 YYYY-MM-DD.
|
|
3
|
+
export function localDate(now = new Date()) {
|
|
4
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
5
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
6
|
+
}
|
|
7
|
+
// YYYY-MM-DD 날짜에서 days일 뒤의 YYYY-MM-DD를 구한다. 시간대와 무관하게 UTC 달력으로 계산한다.
|
|
8
|
+
export function addDays(date, days) {
|
|
9
|
+
const [year, month, day] = date.split('-').map(Number);
|
|
10
|
+
const next = new Date(Date.UTC(year, month - 1, day + days));
|
|
11
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
12
|
+
return `${next.getUTCFullYear()}-${pad(next.getUTCMonth() + 1)}-${pad(next.getUTCDate())}`;
|
|
13
|
+
}
|
|
14
|
+
// 윤년을 포함한 실제 달력 날짜인지 본다. Date 객체를 쓰지 않는다(두 자리 연도 보정 같은 함정을 피한다).
|
|
15
|
+
export function isCalendarDate(year, month, day) {
|
|
16
|
+
if (month < 1 || month > 12 || day < 1)
|
|
17
|
+
return false;
|
|
18
|
+
const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
19
|
+
const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
|
|
20
|
+
return day <= daysInMonth;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=dates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dates.js","sourceRoot":"","sources":["../../src/dates.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAE/B,wBAAwB;AACxB,MAAM,UAAU,SAAS,CAAC,MAAY,IAAI,IAAI,EAAE;IAC9C,MAAM,GAAG,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9D,OAAO,GAAG,GAAG,CAAC,WAAW,EAAE,IAAI,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;AACjF,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,OAAO,CAAC,IAAY,EAAE,IAAY;IAChD,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACvD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC;IAC7D,MAAM,GAAG,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9D,OAAO,GAAG,IAAI,CAAC,cAAc,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,EAAE,CAAC;AAC7F,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,KAAa,EAAE,GAAW;IACrE,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,EAAE,IAAI,GAAG,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACrD,MAAM,IAAI,GAAG,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,GAAG,GAAG,KAAK,CAAC,CAAC,IAAI,IAAI,GAAG,GAAG,KAAK,CAAC,CAAC;IACtE,MAAM,WAAW,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IAC5F,OAAO,GAAG,IAAI,WAAW,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { VOCAB } from './headings.js';
|
|
4
|
+
import { buildSections, classifyTarget, evidenceCandidates, normTitle, scanMarkdown, } from './markdown.js';
|
|
5
|
+
export function vocabOf(config) {
|
|
6
|
+
return { ...VOCAB[config.lang], ...config.headings };
|
|
7
|
+
}
|
|
8
|
+
export function relPath(root, abs) {
|
|
9
|
+
return relative(root, abs).split(sep).join('/');
|
|
10
|
+
}
|
|
11
|
+
export function isFile(path) {
|
|
12
|
+
try {
|
|
13
|
+
return statSync(path).isFile();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function pathExists(path) {
|
|
20
|
+
try {
|
|
21
|
+
statSync(path);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function isMissingDir(error) {
|
|
29
|
+
const code = typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
|
|
30
|
+
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
31
|
+
}
|
|
32
|
+
function realpathOf(path) {
|
|
33
|
+
try {
|
|
34
|
+
return realpathSync.native(path);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// specDir 아래 .md 파일을 찾는다. node_modules와 점으로 시작하는 디렉터리는 들어가지 않는다.
|
|
41
|
+
// 심볼릭 링크는 가리키는 대상의 종류로 판정한다. 실제 경로 기준으로 이미 방문한 디렉터리는 다시 들어가지 않아 순환을 끊는다.
|
|
42
|
+
function listSpecFiles(dirAbs, relDir, out, visited = new Set()) {
|
|
43
|
+
let entries;
|
|
44
|
+
try {
|
|
45
|
+
entries = readdirSync(dirAbs, { withFileTypes: true });
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (isMissingDir(error))
|
|
49
|
+
return;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
const real = realpathOf(dirAbs);
|
|
53
|
+
if (real === undefined || visited.has(real))
|
|
54
|
+
return;
|
|
55
|
+
visited.add(real);
|
|
56
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const rel = relDir === '' ? entry.name : `${relDir}/${entry.name}`;
|
|
59
|
+
let kind;
|
|
60
|
+
if (entry.isSymbolicLink()) {
|
|
61
|
+
try {
|
|
62
|
+
const target = statSync(join(dirAbs, entry.name));
|
|
63
|
+
kind = target.isDirectory() ? 'dir' : target.isFile() ? 'file' : undefined;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
kind = undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (entry.isDirectory()) {
|
|
70
|
+
kind = 'dir';
|
|
71
|
+
}
|
|
72
|
+
else if (entry.isFile()) {
|
|
73
|
+
kind = 'file';
|
|
74
|
+
}
|
|
75
|
+
if (kind === 'dir') {
|
|
76
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.'))
|
|
77
|
+
continue;
|
|
78
|
+
listSpecFiles(join(dirAbs, entry.name), rel, out, visited);
|
|
79
|
+
}
|
|
80
|
+
else if (kind === 'file' && entry.name.toLowerCase().endsWith('.md')) {
|
|
81
|
+
out.push(rel);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// exclude 항목은 스펙 루트 기준 경로다. "/"로 끝나면 디렉터리 전체를, 아니면 파일 또는 디렉터리 하나를 가리킨다.
|
|
86
|
+
function isExcluded(rel, exclude) {
|
|
87
|
+
return exclude.some((entry) => entry.endsWith('/') ? rel.startsWith(entry) : rel === entry || rel.startsWith(`${entry}/`));
|
|
88
|
+
}
|
|
89
|
+
// start 자신(포함 여부는 호출자가 정함)에서 위로 올라가며 specAbs까지의 디렉터리를 가까운 순서로 준다.
|
|
90
|
+
function dirsUpTo(start, specAbs) {
|
|
91
|
+
const out = [];
|
|
92
|
+
let current = start;
|
|
93
|
+
while (current === specAbs || current.startsWith(specAbs + sep)) {
|
|
94
|
+
out.push(current);
|
|
95
|
+
if (current === specAbs)
|
|
96
|
+
break;
|
|
97
|
+
current = dirname(current);
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
export function buildGraph(root, config) {
|
|
102
|
+
const specAbs = resolve(root, config.specDir);
|
|
103
|
+
const indexAbs = join(specAbs, config.index);
|
|
104
|
+
const logAbs = join(specAbs, config.log);
|
|
105
|
+
const codeRootAbs = resolve(root, config.codeRoot);
|
|
106
|
+
const vocab = vocabOf(config);
|
|
107
|
+
const roleOf = (abs) => {
|
|
108
|
+
if (abs === indexAbs)
|
|
109
|
+
return 'index';
|
|
110
|
+
if (abs === logAbs)
|
|
111
|
+
return 'log';
|
|
112
|
+
if (basename(abs) === config.overview)
|
|
113
|
+
return 'overview';
|
|
114
|
+
return 'detail';
|
|
115
|
+
};
|
|
116
|
+
const relFiles = [];
|
|
117
|
+
listSpecFiles(specAbs, '', relFiles);
|
|
118
|
+
const docs = [];
|
|
119
|
+
for (const rel of relFiles) {
|
|
120
|
+
if (isExcluded(rel, config.exclude))
|
|
121
|
+
continue;
|
|
122
|
+
const abs = join(specAbs, rel);
|
|
123
|
+
const scan = scanMarkdown(readFileSync(abs, 'utf8'));
|
|
124
|
+
const sections = buildSections(scan, vocab);
|
|
125
|
+
// 미확정 정책 마커는 확인 필요 사항 절에 적힌 것을 센다. 그 절에 마커가 없으면 문서 전체의 마커를 센다.
|
|
126
|
+
const questions = sections.find((section) => section.key === 'questions');
|
|
127
|
+
const markersInQuestions = questions
|
|
128
|
+
? scan.markerLines.filter((line) => line >= questions.startLine && line <= questions.endLine)
|
|
129
|
+
: [];
|
|
130
|
+
const markers = markersInQuestions.length > 0 ? markersInQuestions : scan.markerLines;
|
|
131
|
+
const links = scan.links.map((link) => {
|
|
132
|
+
const target = classifyTarget(link.rawTarget, abs, root);
|
|
133
|
+
if (target.kind === 'external') {
|
|
134
|
+
return { line: link.line, text: link.text, rawTarget: link.rawTarget, kind: 'external' };
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
line: link.line,
|
|
138
|
+
text: link.text,
|
|
139
|
+
rawTarget: link.rawTarget,
|
|
140
|
+
kind: 'internal',
|
|
141
|
+
path: target.path,
|
|
142
|
+
fragment: target.fragment,
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
const evidencePaths = evidenceCandidates(scan, sections).map((candidate) => ({
|
|
146
|
+
line: candidate.line,
|
|
147
|
+
raw: candidate.raw,
|
|
148
|
+
resolved: resolve(codeRootAbs, candidate.path),
|
|
149
|
+
}));
|
|
150
|
+
const policies = sections
|
|
151
|
+
.filter((section) => section.key === 'policies')
|
|
152
|
+
.flatMap((section) => section.children
|
|
153
|
+
.filter((child) => child.level === 3)
|
|
154
|
+
.map((heading) => ({ heading, normalized: normTitle(heading.text) })));
|
|
155
|
+
// 흐름 신호는 현재 동작과 흐름 절 안의 줄만 센다(절이 없으면 0).
|
|
156
|
+
const inRange = (line, s) => s !== undefined && line >= s.startLine && line <= s.endLine;
|
|
157
|
+
const behavior = sections.find((section) => section.key === 'behavior');
|
|
158
|
+
const flow = sections.find((section) => section.key === 'flow');
|
|
159
|
+
const behaviorBullets = scan.bulletLines.filter((line) => inRange(line, behavior)).length;
|
|
160
|
+
const mermaidFences = scan.mermaidFenceLines.filter((line) => inRange(line, behavior) || inRange(line, flow)).length;
|
|
161
|
+
const tableRows = scan.tableRuleLines.filter((line) => inRange(line, behavior) || inRange(line, flow)).length;
|
|
162
|
+
docs.push({
|
|
163
|
+
file: relPath(root, abs),
|
|
164
|
+
abs,
|
|
165
|
+
role: roleOf(abs),
|
|
166
|
+
title: scan.headings[0]?.level === 1 ? scan.headings[0] : undefined,
|
|
167
|
+
headings: scan.headings,
|
|
168
|
+
sections,
|
|
169
|
+
links,
|
|
170
|
+
evidencePaths,
|
|
171
|
+
policies,
|
|
172
|
+
markerCount: markers.length,
|
|
173
|
+
firstMarkerLine: markers[0],
|
|
174
|
+
flow: { mermaidFences, tableRows, behaviorBullets },
|
|
175
|
+
unterminatedComment: scan.unterminatedComment,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
docs.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
|
|
179
|
+
const byAbs = new Map(docs.map((doc) => [doc.abs, doc]));
|
|
180
|
+
const index = byAbs.get(indexAbs);
|
|
181
|
+
// 링크 대상이 문서 목록에 없지만 디스크에 파일로 있으면, 실제 경로가 문서와 같은지 본다.
|
|
182
|
+
// 같으면 철자가 다른 것(대소문자 차이 등)이므로 링크를 실제 문서로 돌리고 철자 차이를 기록한다.
|
|
183
|
+
const byReal = new Map();
|
|
184
|
+
for (const doc of docs) {
|
|
185
|
+
const real = realpathOf(doc.abs);
|
|
186
|
+
if (real !== undefined)
|
|
187
|
+
byReal.set(real, doc);
|
|
188
|
+
}
|
|
189
|
+
for (const doc of docs) {
|
|
190
|
+
for (const link of doc.links) {
|
|
191
|
+
if (link.kind !== 'internal' || link.path === undefined || byAbs.has(link.path) || !isFile(link.path))
|
|
192
|
+
continue;
|
|
193
|
+
const real = realpathOf(link.path);
|
|
194
|
+
const actual = real === undefined ? undefined : byReal.get(real);
|
|
195
|
+
if (actual === undefined)
|
|
196
|
+
continue;
|
|
197
|
+
link.spellingMismatch = actual.file;
|
|
198
|
+
link.path = actual.abs;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// 개요 디렉터리 → 개요 문서. 상세 문서와 개요는 가까운 개요를 부모로 삼고, 없으면 인덱스가 부모다.
|
|
202
|
+
const overviewByDir = new Map();
|
|
203
|
+
for (const doc of docs) {
|
|
204
|
+
if (doc.role === 'overview')
|
|
205
|
+
overviewByDir.set(dirname(doc.abs), doc);
|
|
206
|
+
}
|
|
207
|
+
for (const doc of docs) {
|
|
208
|
+
if (doc.role === 'index' || doc.role === 'log')
|
|
209
|
+
continue;
|
|
210
|
+
const dir = dirname(doc.abs);
|
|
211
|
+
const start = doc.role === 'overview' ? dirname(dir) : dir;
|
|
212
|
+
let parent;
|
|
213
|
+
for (const candidate of dirsUpTo(start, specAbs)) {
|
|
214
|
+
const overview = overviewByDir.get(candidate);
|
|
215
|
+
if (overview) {
|
|
216
|
+
parent = overview;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
doc.parent = parent ?? index;
|
|
221
|
+
}
|
|
222
|
+
return { root, config, lang: config.lang, vocab, specAbs, indexAbs, logAbs, docs, byAbs, index, slugCache: new Map() };
|
|
223
|
+
}
|
|
224
|
+
// 문서 또는 임의의 .md 파일의 제목 슬러그 집합. 그래프 밖 파일은 한 번만 읽는다.
|
|
225
|
+
export function slugsOf(graph, abs) {
|
|
226
|
+
const known = graph.byAbs.get(abs);
|
|
227
|
+
if (known)
|
|
228
|
+
return new Set(known.headings.map((heading) => heading.slug));
|
|
229
|
+
if (!graph.slugCache.has(abs)) {
|
|
230
|
+
let slugs;
|
|
231
|
+
try {
|
|
232
|
+
slugs = new Set(scanMarkdown(readFileSync(abs, 'utf8')).headings.map((heading) => heading.slug));
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
slugs = undefined;
|
|
236
|
+
}
|
|
237
|
+
graph.slugCache.set(abs, slugs);
|
|
238
|
+
}
|
|
239
|
+
return graph.slugCache.get(abs);
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=graph.js.map
|