@pghoya2956/livemap 1.2.0 → 1.3.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/src/check.mjs CHANGED
@@ -5,6 +5,8 @@ import { problem } from './lib/issues.mjs';
5
5
 
6
6
  // derive가 만든 완성 문장을 코드로 가른다. 맞는 규칙이 없으면 마지막 대체 코드
7
7
  const STEP_WARNING = [[/^라우트 없음/, 'step.route-missing'], [/^참조 미해결/, 'step.ref-unresolved'], [/^장면은 동작인데/, 'step.screen-not-live'], [/^장면은 \S+인데 화면은 동작/, 'step.screen-live-early'], [/관측 근거 없음/, 'step.no-evidence'], [/^확인 필요/, 'step.review-stale']];
8
+ // 로드맵 problem 중 경고로 내는 문장(1.3.0). 선행 흐름 문제라 화면은 멈추지 않고 CI도 막지 않는다
9
+ const ROADMAP_WARNING = [[/^선행 순환/, 'roadmap.dep-cycle'], [/^마일스톤 순서 역행/, 'roadmap.milestone-backward']];
8
10
  const ROADMAP_PROBLEM = [[/^장면 없음/, 'roadmap.scene-missing'], [/^작업 폴더 없음/, 'roadmap.task-missing'], [/^선행 항목 없음/, 'roadmap.dep-missing'], [/^알 수 없는 상태/, 'roadmap.unknown-status'], [/^마일스톤 없음/, 'roadmap.milestone-missing']];
9
11
  const MILESTONE_PROBLEM = [[/: id 없음$/, 'milestone.no-id'], [/^마일스톤 id 중복/, 'milestone.duplicate-id'], [/: 알 수 없는 상태 /, 'milestone.unknown-status'], [/: 날짜 형식 /, 'milestone.bad-date']];
10
12
  const MILESTONE_WARNING = [[/: 완료인데 미완료 항목/, 'milestone.done-open-items'], [/: 항목이 모두 완료인데 상태/, 'milestone.all-items-done'], [/: 진행 항목이 있는데 상태/, 'milestone.running-items'], [/: 묶인 항목 없음$/, 'milestone.no-items'], [/: 완료일과 상태가 맞지 않음$/, 'milestone.completed-on-mismatch']];
@@ -42,7 +44,11 @@ export function checkProblems(d, cfg) {
42
44
  for (const m of d.roadmap || []) {
43
45
  const ms = subj('milestone', m.id);
44
46
  if (mids.has(m.id)) err('roadmap.duplicate-id', `로드맵 id 중복: ${m.id}`, ms); mids.add(m.id);
45
- for (const p of m.problems) err(pick(ROADMAP_PROBLEM, p, 'roadmap.problem'), `로드맵 ${m.title}: ${p}`, ms);
47
+ for (const p of m.problems) {
48
+ const w = pick(ROADMAP_WARNING, p, null);
49
+ if (w) warn(w, `로드맵 ${m.title}: ${p}`, ms);
50
+ else err(pick(ROADMAP_PROBLEM, p, 'roadmap.problem'), `로드맵 ${m.title}: ${p}`, ms);
51
+ }
46
52
  if (m.status === '진행' && !m.tasks.length) warn('roadmap.running-no-task', `로드맵 ${m.title}: 진행인데 작업 폴더가 없음`, ms);
47
53
  }
48
54
  // 마일스톤: 문장은 derive가 만든다. 마일스톤 절이 없으면 줄이 없다
package/src/derive.mjs CHANGED
@@ -39,6 +39,44 @@ export function splitWaiting(text) {
39
39
  return m ? { who: m[1].trim(), what: m[2] } : { who: null, what: t };
40
40
  }
41
41
 
42
+ // 로드맵 선행의 흐름 문제(1.3.0): 선행 순환과 마일스톤 순서 역행. 화면 ui/lib/tree.js와 같은 규칙이고 check는 경고로 낸다.
43
+ // 순환: 자기에게 되돌아오는 항목마다 자기에서 시작해 선행 → 후속 방향으로 돌아오는 가장 짧은 경로 한 문장.
44
+ // 역행: 마일스톤이 있을 때 선행의 열(파일 순서, 미배정·없는 id는 맨 오른쪽)이 항목의 열보다 오른쪽이면 엣지마다 한 문장.
45
+ function roadmapFlowProblems(items, releases) {
46
+ const out = new Map();
47
+ const add = (id, text) => { if (!out.has(id)) out.set(id, []); out.get(id).push(text); };
48
+ const ids = new Set(items.map((m) => m.id));
49
+ const deps = new Map(items.map((m) => [m.id, [...new Set(m.props.deps)].filter((d) => ids.has(d))]));
50
+ const children = new Map(items.map((m) => [m.id, []]));
51
+ for (const m of items) for (const d of deps.get(m.id)) children.get(d).push(m.id);
52
+ for (const m of items) {
53
+ const prev = new Map([[m.id, null]]);
54
+ const queue = [m.id];
55
+ let back = null;
56
+ for (let i = 0; i < queue.length && !back; i++) {
57
+ for (const c of children.get(queue[i])) {
58
+ if (c === m.id) { back = queue[i]; break; }
59
+ if (!prev.has(c)) { prev.set(c, queue[i]); queue.push(c); }
60
+ }
61
+ }
62
+ if (!back) continue;
63
+ const path = [m.id];
64
+ for (let x = back; x !== m.id; x = prev.get(x)) path.splice(1, 0, x);
65
+ add(m.id, `선행 순환: ${[...path, m.id].join(' → ')}`);
66
+ }
67
+ if (releases.length) {
68
+ const col = new Map(releases.map((r, i) => [r.id, i]));
69
+ const colOf = (m) => col.get(m.props.milestone) ?? releases.length;
70
+ const label = (m) => (col.has(m.props.milestone) ? m.props.milestone : '마일스톤 없음');
71
+ const byId = new Map(items.map((m) => [m.id, m]));
72
+ for (const m of items) for (const d of deps.get(m.id)) {
73
+ const p = byId.get(d);
74
+ if (colOf(p) > colOf(m)) add(m.id, `마일스톤 순서 역행: ${d}(${label(p)}) → ${m.id}(${label(m)})`);
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+
42
80
  export function derive(g, sem, cfg, { captureExists }) {
43
81
  const screens = g.of('screen'), apis = g.of('api'), fns = g.of('function'), tests = g.of('test'), commits = g.of('commit').sort((a, b) => b.props.date.localeCompare(a.props.date));
44
82
  const tasks = g.of('task'), decisions = g.of('decision').filter((d) => d.props.kind === 'wiki'), specDefs = g.of('decision').filter((d) => d.props.kind !== 'wiki');
@@ -181,6 +219,7 @@ export function derive(g, sem, cfg, { captureExists }) {
181
219
  const milestoneIds = new Set(milestones.map((m) => m.id));
182
220
  const releases = g.of('release').sort((a, b) => a.props.order - b.props.order);
183
221
  const releaseIds = new Set(releases.map((r) => r.id));
222
+ const flowProblems = roadmapFlowProblems(milestones, releases);
184
223
  const roadmap = milestones.map((m) => {
185
224
  const p = m.props;
186
225
  const scenes = p.scenes.map((r) => stepByRef[r] ? { ref: r, ...stepByRef[r] } : { ref: r, missing: true });
@@ -188,6 +227,7 @@ export function derive(g, sem, cfg, { captureExists }) {
188
227
  const problems = [...scenes.filter((s) => s.missing).map((s) => `장면 없음: ${s.ref}`), ...trackedTasks.filter((t) => t.missing).map((t) => `작업 폴더 없음: ${t.name}`), ...p.deps.filter((d) => !milestoneIds.has(d)).map((d) => `선행 항목 없음: ${d}`)];
189
228
  if (!ROADMAP_STATUS.includes(p.status)) problems.push(`알 수 없는 상태: ${p.status || '(비어 있음)'}`);
190
229
  if (p.milestone && !releaseIds.has(p.milestone)) problems.push(`마일스톤 없음 ${p.milestone}`);
230
+ problems.push(...(flowProblems.get(m.id) || []));
191
231
  const live = scenes.filter((s) => s.status === 'live').length;
192
232
  const deps = p.deps.map((d) => ({ id: d, title: milestones.find((x) => x.id === d)?.label || d, status: milestones.find((x) => x.id === d)?.props.status || null }));
193
233
  const waiting = splitWaiting(p.waitingOn);
@@ -315,10 +355,16 @@ export function overviewSlice(d, opts = {}) {
315
355
  const currentMilestone = (milestones.find((m) => m.status === '진행') || milestones.find((m) => m.status === '다음'))?.id ?? null;
316
356
  const openItems = (d.roadmap || []).filter((r) => r.status !== '완료').sort((a, b) => a.order - b.order);
317
357
 
358
+ // 캡처: 기능마다 동작 단계의 캡처 파일을 단계 순서로 최대 5장. 중복 제거는 기능 안에서만 하고 전체 상한은 없다(1.3.0, DEC-23)
318
359
  const captures = [];
319
360
  for (const j of d.semantic.journeys) {
320
- const s = j.steps.find((x) => x.status === 'live' && x.captureFile && !captures.some((c) => c.file === x.captureFile));
321
- if (s && captures.length < 5) captures.push({ file: s.captureFile, journey: j.id, step: s.id });
361
+ const files = new Set();
362
+ for (const s of j.steps) {
363
+ if (files.size >= 5) break;
364
+ if (s.status !== 'live' || !s.captureFile || files.has(s.captureFile)) continue;
365
+ files.add(s.captureFile);
366
+ captures.push({ file: s.captureFile, journey: j.id, step: s.id });
367
+ }
322
368
  }
323
369
  const changes = human.slice().sort((a, b) => Date.parse(b.date) - Date.parse(a.date)).slice(0, 20).map((c) => {
324
370
  const touched = new Set(c.journeys.map((x) => x.journey));
@@ -57,6 +57,9 @@ export const ISSUE_CODES = {
57
57
  'roadmap.running-no-task': code('warn', '로드맵 항목', ['source']),
58
58
  'roadmap.running-no-milestone': code('warn', '로드맵 항목', ['source']),
59
59
  'roadmap.running-open-deps': code('warn', '로드맵 항목', ['source']),
60
+ // 1.3.0: 선행 흐름 경고
61
+ 'roadmap.dep-cycle': code('warn', '로드맵 항목', ['source']),
62
+ 'roadmap.milestone-backward': code('warn', '로드맵 항목', ['source']),
60
63
  'milestone.no-id': code('error', '마일스톤', ['source']),
61
64
  'milestone.duplicate-id': code('error', '마일스톤', ['source']),
62
65
  'milestone.unknown-status': code('error', '마일스톤', ['source']),