@pghoya2956/livemap 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +6 -0
- package/budget/view-budget.spec.mjs +75 -23
- package/docs/adapter-contract.md +23 -4
- package/docs/hosting-and-csp.md +26 -2
- package/docs/migrate.md +6 -1
- package/docs/semantic-authoring.md +115 -2
- package/docs/semantic-schema.md +21 -5
- package/docs/view-budget.md +38 -16
- package/package.json +40 -10
- package/site/index.html +3 -15
- package/site/map.css +439 -155
- package/site/map.js +61 -199
- package/src/adapters/roadmap.mjs +89 -19
- package/src/check.mjs +18 -0
- package/src/cli.mjs +1 -1
- package/src/derive.mjs +163 -4
- package/src/lib/graph.mjs +13 -3
- package/templates/README.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,58 @@
|
|
|
2
2
|
|
|
3
3
|
버전마다 `## [X.Y.Z] - YYYY-MM-DD` 절을 둔다. 릴리스 워크플로가 태그 버전의 절이 있는지 확인한다.
|
|
4
4
|
|
|
5
|
+
## [1.1.0] - 2026-09-17
|
|
6
|
+
|
|
7
|
+
상황판 화면을 React 다크 모니터형으로 다시 만들고, 로드맵 항목을 묶는 마일스톤과 날짜·결정 대기 자료를 더했다. README 「버전」이 major로 정한 항목(config 키, 여정 형식, 어댑터 계약, 명령·종료 코드, 생성물 파일 이름, export 배치, 예산 설정 경로)은 바꾸거나 지우지 않고, 로드맵 형식·어댑터 계약·생성물은 추가만 했다.
|
|
8
|
+
|
|
9
|
+
화면
|
|
10
|
+
|
|
11
|
+
- 화면 원본을 `ui/`(React)로 옮기고 `scripts/build-ui.mjs`(esbuild)가 `site/index.html`·`map.css`·`map.js`로 번들한다. 파일 이름·위치와 export 배치는 같다. 번들은 커밋하고 CI·릴리스가 다시 빌드해 차이가 없는지 본다. React는 번들 안에 있고 런타임 의존성은 계속 0이다. 번들 끝에 React MIT 고지를 남긴다. `map.js`는 약 33KB에서 약 250KB(gzip 약 77KB)가 된다.
|
|
12
|
+
- 개요: 상단 바(내비 5·배포·경고·신선도 알약·제품 호스트), 전광판, 8패널(마일스톤, 진척, 최근 변경, 기능 지도, 기능별 변경, 특보, 화면 캡처, 기능 현황). 다크 한 벌이고 글자 대비 4.5:1 이상, 상태는 모양으로도 구분한다. 목록은 들어가는 행만 그리고 나머지는 "외 n →" 링크다.
|
|
13
|
+
- 해시 라우터와 하위 화면(기능 목록·스토리보드·단계 상세, 로드맵, 작업, 더보기 6탭)을 React로 옮겼다. 정보와 배치는 1.0.1과 같고, 로드맵은 마일스톤별로 접고, 작업은 진행·대기가 기본 필터다. 경로(`#/overview`·`#/journeys/…`·`#/roadmap/…`·`#/tasks/…`·`#/more/…`)는 같고 `#/roadmap/<id>`가 마일스톤 id도 받는다.
|
|
14
|
+
- 화면 어휘: 여정 → 기능, 장면 → 단계, `planned` → 계획, `next` → 구상. 파일 키·값과 프로젝트 문구는 그대로다.
|
|
15
|
+
- 하위 화면의 여정·로드맵 파일 경로 문구를 고정 문자열 대신 설정 값(`data.json.sources`)으로 보인다.
|
|
16
|
+
- 기능이 13개 이상이면 완성 기능을 기능 지도에서 접는다(지도 12개 이하 유지, 기능 화면에는 전부).
|
|
17
|
+
- 전광판 정지 버튼, 모션 줄임, 기능 지도 키보드 선택, 1279px 이하 두 줄 상단 바.
|
|
18
|
+
- 마지막 방문 뒤 커밋 표시가 시각을 문자열로 비교해 `Z`와 `+09:00`이 섞이면 틀리던 것을 `Date.parse` 비교로 고쳤다.
|
|
19
|
+
|
|
20
|
+
로드맵과 마일스톤
|
|
21
|
+
|
|
22
|
+
- 로드맵 파일에 `## 마일스톤: <제목>` 절(`id`·`상태`·`완료일`·`목표일`·`결정 대기`)과 항목 키 `마일스톤`을 더했다. 항목 순서·자동 id는 항목 절만 세어 1.0.1과 같다. 마일스톤 절은 1.1.0 이상에서만 쓴다. 1.0.x는 이 절을 로드맵 항목으로 읽으므로 엔진을 먼저 올린다(`docs/semantic-authoring.md`).
|
|
23
|
+
- 로드맵 어댑터가 로드맵 파일 git 이력에서 항목 완료일(`completedAt`)과 결정 대기 시작일(`waitingSince`)을 계산한다. 이름 바꾸기를 따라가고, 미커밋 변경·git 없음은 null, 얕은 클론은 null과 partial이다.
|
|
24
|
+
- 결정 대기 `<주체>: <질문>`을 주체와 질문으로 나눈다(콜론 뒤 공백 필요).
|
|
25
|
+
- 로드맵 항목 막힘(`blockedBy`), 마일스톤 진척·막힘, 현재 마일스톤을 파생한다.
|
|
26
|
+
|
|
27
|
+
생성물(필드·노드 추가만)
|
|
28
|
+
|
|
29
|
+
- `overview.json`: 첫 화면 자료 전부(`roadmapItems`, `milestones`, `currentMilestone`, `activity`, `changes`, `links`, `captures`, `journeys[]`의 `goal`·`counts`·`roadmapItem`·`milestone`·`commits`·`week`·`series`, 단계 `hits`, `counts`·`signals` 추가 키). 커밋 제목은 사람 커밋만, 관례 접두어·내부 ID·식별자를 지워 싣고 `[bot]` 작성자 커밋은 자동 수로 따로 센다. 크기는 기능·커밋 수에 따라 커진다(엔진 픽스처 3,697B, 실제 프로젝트 하나에서 25,637B).
|
|
30
|
+
- `data.json`: `roadmap[]`의 `milestone`·`completedAt`·`waitingSince`·`waitingWho`·`waitingWhat`·`blockedBy`, `milestones[]`, `issues[]`, `sources`, `tasks[].roadmapItems`. 기존 경로·값과 `commits` 순서는 그대로다.
|
|
31
|
+
- `graph.json`: 노드 종류 `release`(마일스톤, 1.x 임시 이름), `release → milestone` `contains` 엣지, 로드맵 항목 노드 속성, 최상위 `issues[]`.
|
|
32
|
+
- `overviewSlice(d, opts)`가 선택 인자 `opts.sinceDays`(기본 14)를 받는다.
|
|
33
|
+
- 1.0.1이 만들던 필드 중 새 화면이 쓰지 않는 것(`line`, `running`, `waiting`, `tasks`, `openQuestions`, `areas`, `recent`, `roadmap[]`)도 남긴다. 정리는 2.0.0 후보다.
|
|
34
|
+
|
|
35
|
+
어댑터 계약
|
|
36
|
+
|
|
37
|
+
- `g.issue(level, label, message)`: 프로젝트 어댑터가 `'error'`·`'warn'`을 낸다. 실행 중 어댑터 이름과 함께 `issues[]`에 남고, `check`가 `✗`·`△` 줄로 출력하며(error는 종료 코드 1), 더보기 > 이 상황판에 목록으로 나온다.
|
|
38
|
+
|
|
39
|
+
check
|
|
40
|
+
|
|
41
|
+
- 마일스톤 규칙 오류 5종·경고 7종(마일스톤 절이 있을 때만, 진행 항목에 마일스톤 키 없음 포함), 진행 항목의 선행 미완 경고, 배우 사전에 없는 배우 경고, `g.issue` 줄. 새 경고는 종료 코드를 바꾸지 않는다.
|
|
42
|
+
|
|
43
|
+
예산 검사와 CI
|
|
44
|
+
|
|
45
|
+
- 예산 검사 선택자를 새 화면에 맞췄다(`body` 배경, `#root` 식별자, 기능 지도 → 기능 화면 → 단계 → 상세 3번 클릭, 내비 링크 모두 보임). 숨은 스크롤 0, 모션 줄임 애니메이션 0, 상태 모양 검사를 더했다. 임계값·설정 키·경로·스크린샷 경로는 같고, 스크린샷은 모션 줄임에서 서체를 기다린 뒤 찍는다.
|
|
46
|
+
- 스모크가 하위 화면 전 경로 콘솔 오류, 설정 경로를 바꾼 픽스처의 화면 문구, 클릭 경로 크롤(`scripts/route-crawl.mjs`: 개요 클릭 대상과 라우트 패턴별 모든 개체를 `serve`·`serve --static` 두 방식으로 방문)을 본다.
|
|
47
|
+
|
|
48
|
+
문서
|
|
49
|
+
|
|
50
|
+
- `docs/view-budget.md`(첫 화면 질문, 새 규칙, 패널 바꾸는 절차), `docs/hosting-and-csp.md`(React 클라이언트 렌더와 CSSOM, 배포 뒤 검증, 게이트 뒤 확인 방법), `docs/semantic-authoring.md`(화면 어휘, 배우 사전, 로드맵·마일스톤 작성), `docs/semantic-schema.md`, `docs/adapter-contract.md`, `docs/migrate.md`(전환 전후 측정).
|
|
51
|
+
|
|
52
|
+
## [1.0.1] - 2026-09-17
|
|
53
|
+
|
|
54
|
+
- 문서: `npx`로 옵션을 넘길 때 `--`가 필요하다는 안내, 로컬 엔진을 `--install-links`로 끼워 보는 방법을 README에 더했다.
|
|
55
|
+
- 이 판부터 태그 push로 GitHub Actions 신뢰 배포(provenance 포함)로 공개한다.
|
|
56
|
+
|
|
5
57
|
## [1.0.0] - 2026-09-17
|
|
6
58
|
|
|
7
59
|
첫 공개 판. 한 프로젝트 저장소 안에 있던 상황판 엔진을 패키지로 옮겼다.
|
package/README.md
CHANGED
|
@@ -29,6 +29,12 @@ npx --no livemap init # map/ 초안·.gitignore·npm 스크립
|
|
|
29
29
|
|
|
30
30
|
명령은 프로젝트 루트에서 부른다. CI에서는 npm 스크립트나 `npx --no livemap`을 쓴다.
|
|
31
31
|
|
|
32
|
+
`npx`로 부를 때 엔진 옵션은 `--` 뒤에 둔다. `npx --no livemap --version`은 `--version`을 npx가 가져가 npm 버전을 출력한다. `npx --no livemap -- --version` 또는 `node_modules/.bin/livemap --version`을 쓴다.
|
|
33
|
+
|
|
34
|
+
## 로컬 엔진 끼워 보기
|
|
35
|
+
|
|
36
|
+
엔진 저장소에서 고친 판을 쓰는 프로젝트에서 확인할 때는 `npm install --no-save --install-links <엔진 저장소 경로>`를 쓴다. `package.json`·lockfile은 바뀌지 않고, `npm ci`가 끼운 판을 걷어낸다. `--install-links` 없이 폴더를 설치하면 심링크가 되어 화면 예산 설정이 `@playwright/test`를 엔진 저장소 쪽에서 찾다 실패한다.
|
|
37
|
+
|
|
32
38
|
## 설정
|
|
33
39
|
|
|
34
40
|
`map/config.json` 하나가 프로젝트별이다. 참조 어댑터는 React Router + Node BFF + SQL migration + Markdown 작업 문서 관례를 읽는다. 스택이 다르면 `map/adapters/<이름>.mjs`에 프로젝트 어댑터를 둔다.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// 화면 예산 검사: 첫 화면(개요)이 단순함 규칙을 지키는지 잰다. 어기면 CI가 실패한다.
|
|
2
|
-
// 1440×900에서 스크롤 없음 · 패널 8 이하 · 목록
|
|
2
|
+
// CSP 아래 오류 0(바탕 --bg) · 1440×900에서 스크롤 없음 · 패널 8 이하 · 목록 묶음마다 6행 이하 · 기능 지도 12행 이하
|
|
3
|
+
// · 내비 5가 모두 보임 · #root 글자에 시스템 식별자 0 · 기능 지도 선택 → a.open → 단계 카드 → 단계 상세 3번 안
|
|
4
|
+
// · 패널 숨은 스크롤 0 · 모션 줄임에서 애니메이션 0 · 범례 상태 모양 4종 구분 · 모션 줄임 스크린샷
|
|
5
|
+
// 임계값은 map/config.json의 budget(viewport·maxPanels·maxRowsPerPanel·navItems)이다.
|
|
3
6
|
import { test, expect } from '@playwright/test';
|
|
4
7
|
import { readFileSync } from 'node:fs';
|
|
5
8
|
import { resolve } from 'node:path';
|
|
@@ -8,35 +11,41 @@ import { resolve } from 'node:path';
|
|
|
8
11
|
const cfg = JSON.parse(readFileSync(resolve(process.cwd(), 'map/config.json'), 'utf8')).budget;
|
|
9
12
|
const IDENT = /\/api\/|\.tsx\b|\.mjs\b|\.sql\b|\bweb\/src\b|\b[0-9a-f]{7,40}\b/;
|
|
10
13
|
|
|
14
|
+
const openOverview = async (page) => {
|
|
15
|
+
await page.goto('/map/#/overview');
|
|
16
|
+
await page.waitForSelector('.panel', { timeout: 10_000 });
|
|
17
|
+
await page.evaluate(() => document.fonts.ready);
|
|
18
|
+
};
|
|
19
|
+
|
|
11
20
|
test('CSP 아래서 오류 없이 렌더된다', async ({ page }) => {
|
|
12
21
|
const errors = [];
|
|
13
22
|
page.on('pageerror', (e) => errors.push(String(e.message)));
|
|
14
23
|
page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });
|
|
15
|
-
await page.
|
|
16
|
-
await page
|
|
24
|
+
await page.addInitScript(() => document.addEventListener('securitypolicyviolation', (e) => console.error(`CSP ${e.violatedDirective} ${e.blockedURI}`)));
|
|
25
|
+
await openOverview(page);
|
|
17
26
|
expect(errors, errors.join('\n')).toEqual([]);
|
|
18
|
-
const bg = await page.evaluate(() => getComputedStyle(document.
|
|
19
|
-
expect(bg).
|
|
27
|
+
const bg = await page.evaluate(() => getComputedStyle(document.body).backgroundColor);
|
|
28
|
+
expect(bg).toBe('rgb(5, 7, 10)');
|
|
20
29
|
});
|
|
21
30
|
|
|
22
31
|
test('개요는 한 화면에 들어간다', async ({ page }) => {
|
|
23
|
-
await page
|
|
24
|
-
await page.waitForSelector('.panel');
|
|
32
|
+
await openOverview(page);
|
|
25
33
|
const [scrollH, clientH] = await page.evaluate(() => [document.documentElement.scrollHeight, document.documentElement.clientHeight]);
|
|
26
34
|
expect(scrollH, `스크롤 높이 ${scrollH} > 뷰포트 ${clientH}`).toBeLessThanOrEqual(clientH);
|
|
27
35
|
const bodyW = await page.evaluate(() => document.documentElement.scrollWidth);
|
|
28
36
|
expect(bodyW).toBeLessThanOrEqual(cfg.viewport[0]);
|
|
29
37
|
});
|
|
30
38
|
|
|
31
|
-
test('패널·행·내비 수가 예산
|
|
32
|
-
await page
|
|
33
|
-
await page.waitForSelector('.panel');
|
|
39
|
+
test('패널·행·내비 수가 예산 안이고 내비가 모두 보인다', async ({ page }) => {
|
|
40
|
+
await openOverview(page);
|
|
34
41
|
expect(await page.locator('.panel').count()).toBeLessThanOrEqual(cfg.maxPanels);
|
|
35
|
-
|
|
42
|
+
const nav = page.locator('.nav a');
|
|
43
|
+
expect(await nav.count()).toBeLessThanOrEqual(cfg.navItems);
|
|
44
|
+
for (let i = 0; i < await nav.count(); i += 1) await expect(nav.nth(i)).toBeVisible();
|
|
36
45
|
const lists = page.locator('.panel[data-budget="list"]');
|
|
37
46
|
for (let i = 0; i < await lists.count(); i += 1) {
|
|
38
47
|
const rows = await lists.nth(i).locator('.row, .bar').count();
|
|
39
|
-
// 한 패널에
|
|
48
|
+
// 한 패널에 목록 묶음(.rows)이 여럿이면 묶음마다 6행 이하로 본다.
|
|
40
49
|
const groups = await lists.nth(i).locator('.rows, .bars').count();
|
|
41
50
|
expect(rows, `패널 ${i} 행 ${rows}`).toBeLessThanOrEqual(cfg.maxRowsPerPanel * Math.max(1, groups));
|
|
42
51
|
}
|
|
@@ -45,26 +54,69 @@ test('패널·행·내비 수가 예산 안이다', async ({ page }) => {
|
|
|
45
54
|
});
|
|
46
55
|
|
|
47
56
|
test('첫 화면에는 시스템 식별자가 없다', async ({ page }) => {
|
|
48
|
-
await page
|
|
49
|
-
await page.
|
|
50
|
-
const text = await page.locator('.main').innerText();
|
|
57
|
+
await openOverview(page);
|
|
58
|
+
const text = await page.locator('#root').innerText();
|
|
51
59
|
const hit = text.split('\n').find((l) => IDENT.test(l));
|
|
52
60
|
expect(hit, `식별자 노출: ${hit}`).toBeUndefined();
|
|
53
61
|
});
|
|
54
62
|
|
|
55
|
-
test('
|
|
56
|
-
await page
|
|
57
|
-
await page.waitForSelector('.jrow');
|
|
63
|
+
test('기능 지도 선택 → a.open → 단계 상세까지 3번 안에 닿는다', async ({ page }) => {
|
|
64
|
+
await openOverview(page);
|
|
58
65
|
await page.locator('.jrow').first().click();
|
|
66
|
+
await page.locator('a.open').click();
|
|
59
67
|
await page.waitForSelector('.scene');
|
|
60
68
|
await page.locator('.scene').first().click();
|
|
61
69
|
await page.waitForSelector('.detail');
|
|
62
70
|
expect(await page.locator('.detail .node').count()).toBeGreaterThan(0);
|
|
63
71
|
});
|
|
64
72
|
|
|
65
|
-
test('
|
|
66
|
-
await page
|
|
67
|
-
|
|
68
|
-
await page.
|
|
69
|
-
|
|
73
|
+
test('패널 숨은 스크롤이 없다', async ({ page }) => {
|
|
74
|
+
await openOverview(page);
|
|
75
|
+
// .panel·.pb·.rows와 패널 안 overflow-y auto/scroll 자손. 줄 수 제한 글자와 지도·캡처 상자는 뺀다.
|
|
76
|
+
const hidden = await page.evaluate(() => {
|
|
77
|
+
const set = new Set(document.querySelectorAll('.panel, .pb, .rows'));
|
|
78
|
+
document.querySelectorAll('.panel *').forEach((e) => { const o = getComputedStyle(e).overflowY; if (o === 'auto' || o === 'scroll') set.add(e); });
|
|
79
|
+
return [...set].filter((e) => !e.closest('.mapwrap, .live'))
|
|
80
|
+
.filter((e) => { const c = getComputedStyle(e).webkitLineClamp; return !c || c === 'none'; })
|
|
81
|
+
.filter((e) => e.scrollHeight - e.clientHeight > 1)
|
|
82
|
+
.map((e) => `${e.className} ${e.closest('.panel')?.querySelector('h2')?.textContent || ''} +${e.scrollHeight - e.clientHeight}px`);
|
|
83
|
+
});
|
|
84
|
+
expect(hidden, hidden.join('\n')).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('범례 상태 모양 4종이 서로 다르다', async ({ page }) => {
|
|
88
|
+
await openOverview(page);
|
|
89
|
+
test.skip(await page.locator('.layers svg.stepmark').count() === 0, '기능이 없어 범례가 없다');
|
|
90
|
+
// 동작 = 채운 원(테두리 없음), 목업 = 반원 채움 + 실선, 계획 = 채움 없음 + 실선, 구상 = 채움 없음 + 점선
|
|
91
|
+
const kinds = await page.evaluate(() => {
|
|
92
|
+
const WORD = { 동작: 'live', 목업: 'mock', 계획: 'planned', 구상: 'next' };
|
|
93
|
+
return [...document.querySelectorAll('.layers svg.stepmark')].map((svg) => {
|
|
94
|
+
const word = Object.keys(WORD).find((w) => (svg.closest('button')?.textContent || '').includes(w));
|
|
95
|
+
const shapes = [...svg.querySelectorAll('circle, path')].map((s) => { const cs = getComputedStyle(s); return { tag: s.tagName, fill: cs.fill !== 'none' && cs.fillOpacity !== '0', stroke: cs.stroke !== 'none' && parseFloat(cs.strokeWidth) > 0, dash: cs.strokeDasharray !== 'none' }; });
|
|
96
|
+
const filledCircle = shapes.some((x) => x.tag === 'circle' && x.fill && !x.stroke);
|
|
97
|
+
const filledPath = shapes.some((x) => x.tag === 'path' && x.fill);
|
|
98
|
+
const solid = shapes.some((x) => x.stroke && !x.dash), dashed = shapes.some((x) => x.stroke && x.dash);
|
|
99
|
+
const kind = filledCircle && !solid && !dashed ? 'live' : filledPath && solid ? 'mock' : dashed ? 'next' : solid ? 'planned' : 'unknown';
|
|
100
|
+
return { word, expect: WORD[word], kind };
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
expect(kinds.filter((k) => k.expect).length, JSON.stringify(kinds)).toBe(4);
|
|
104
|
+
for (const k of kinds.filter((x) => x.expect)) expect(k.kind, `${k.word} 표식`).toBe(k.expect);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test.describe('모션 줄임', () => {
|
|
108
|
+
test.use({ reducedMotion: 'reduce' });
|
|
109
|
+
|
|
110
|
+
test('모션 줄임에서 애니메이션이 없다', async ({ page }) => {
|
|
111
|
+
await openOverview(page);
|
|
112
|
+
await page.waitForTimeout(500);
|
|
113
|
+
const names = await page.evaluate(() => document.getAnimations().map((a) => a.animationName || a.constructor.name));
|
|
114
|
+
expect(names, names.join(', ')).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('개요 스크린샷(모션 줄임)', async ({ page }) => {
|
|
118
|
+
await openOverview(page);
|
|
119
|
+
await page.waitForTimeout(300);
|
|
120
|
+
await page.screenshot({ path: resolve(process.cwd(), 'map/.out/overview-1440.png') });
|
|
121
|
+
});
|
|
70
122
|
});
|
package/docs/adapter-contract.md
CHANGED
|
@@ -9,15 +9,30 @@
|
|
|
9
9
|
export default function name(g, fs, cfg) {
|
|
10
10
|
// ... 노드·엣지 추가
|
|
11
11
|
return null; // 정상
|
|
12
|
-
// return '설명'; // partial: 일부만 읽음(예: 파일 없음). 생성은 계속되고
|
|
13
|
-
// throw new Error('…'); // failed:
|
|
12
|
+
// return '설명'; // partial: 일부만 읽음(예: 파일 없음). 생성은 계속되고 더보기 > 이 상황판의 어댑터 상태에 남는다
|
|
13
|
+
// throw new Error('…'); // failed: 개요 특보 "자료 일부 누락" + check 오류. 그래도 다른 어댑터는 돈다
|
|
14
14
|
}
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
- `g` — 그래프. `g.add(kind, id, label, props, src)`, `g.link(fromKind, fromId, edgeKind, toKind, toId)`, `g.get`, `g.of(kind)`, `g.in`, `g.out
|
|
17
|
+
- `g` — 그래프. `g.add(kind, id, label, props, src)`, `g.link(fromKind, fromId, edgeKind, toKind, toId)`, `g.get`, `g.of(kind)`, `g.in`, `g.out`, `g.issue(level, label, message)`(1.1.0부터).
|
|
18
18
|
- `fs` — 저장소 접근. `read(rel)`, `has(rel)`, `isDir(rel)`, `walk(dir, pred)`, `ls(dir)`, `git(...args)`(실패 시 빈 문자열), `hasGit()`, `resolveRef(name)`(main → origin/main → HEAD), `lastCommit(rel)`, `lineOf(text, needle)`.
|
|
19
19
|
- `cfg` — `map/config.json` 전체. 자기 키(`cfg.<name>`)만 읽고, 다른 어댑터의 키는 `?.`로 방어한다.
|
|
20
20
|
|
|
21
|
+
## 오류·경고 보고(g.issue)
|
|
22
|
+
|
|
23
|
+
어댑터가 읽은 사실에서 프로젝트 규칙 위반을 찾았으면 `g.issue(level, label, message)`로 낸다. 반환값 partial·throw는 "어댑터가 제대로 읽었나"를, `g.issue`는 "읽은 내용에 문제가 있나"를 알린다. 1.1.0부터 쓸 수 있다.
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
g.issue('warn', '로드맵', '결정 대기 30일 넘음: 결제 흐름');
|
|
27
|
+
g.issue('error', '여정 파일', '필수 키 없음: owner');
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- `level`은 `'error'` 또는 `'warn'`이다. 그 밖의 값이거나 `label`·`message`가 문자열이 아니면 `throw`하고, 그 어댑터는 failed가 된다. 다른 어댑터는 계속 돈다.
|
|
31
|
+
- 엔진이 지금 실행 중인 어댑터 이름을 함께 기록한다(`config.adapters`의 이름). 어댑터가 throw하기 전에 낸 문제도 남는다.
|
|
32
|
+
- `graph.json`·`data.json`의 최상위 `issues[]`에 `{level, label, message, adapter}`로 남는다.
|
|
33
|
+
- `livemap check`는 기존 검사 뒤에 error를 `✗ {label}: {message}`로, warn을 `△ {label}: {message}`로 출력한다. error는 종료 코드 1에 센다.
|
|
34
|
+
- 개요에는 나오지 않고 더보기 > 이 상황판(`#/more/about`)에 목록으로 나온다.
|
|
35
|
+
|
|
21
36
|
## 어디에 두나
|
|
22
37
|
|
|
23
38
|
엔진은 `config.adapters`의 이름마다 프로젝트 `map/adapters/<name>.mjs`를 먼저 찾고, 없으면 패키지에 딸린 참조 어댑터(`src/adapters/<name>.mjs`)를 쓴다. 프로젝트 파일이 참조 어댑터와 이름이 같으면 `build`·`check`가 "프로젝트 어댑터가 참조 어댑터를 가림: <name>" 한 줄을 알린다. 참조 어댑터의 결함은 엔진 저장소에서 고치고, 프로젝트만의 스택은 다른 이름의 프로젝트 어댑터로 둔다.
|
|
@@ -44,8 +59,12 @@ export default function name(g, fs, cfg) {
|
|
|
44
59
|
| ledger | `running-i`·`waiting-i` | tasks |
|
|
45
60
|
| deploy | `head`·`homelab` | git, deploy |
|
|
46
61
|
| testreport | `last` | testreport |
|
|
62
|
+
| milestone | 로드맵 항목 id(1.x 이름) | roadmap |
|
|
63
|
+
| release | 마일스톤 id(1.x 임시 이름, 1.1.0부터) | roadmap |
|
|
64
|
+
|
|
65
|
+
로드맵 항목과 마일스톤의 노드 종류 이름은 1.x 동안 `milestone`·`release`이고, 2.0.0에서 `roadmapItem`·`milestone`으로 바꾼다(어댑터 계약 변경이라 major).
|
|
47
66
|
|
|
48
|
-
엣지: `shows`(step→screen, 파생이 만든다), `calls`(screen→api), `invokes`(api→function), `touches`(function→table), `covers`(test→screen|api|function), `changes`(commit→screen|api|migration), `defines`(task→decision), `contains`(migration→table|function). 새 종류가 필요하면 엔진 저장소의 `src/lib/graph.mjs` 목록에 더한다(minor 릴리스). 화면이 그 종류를 그리려면 `src/derive.mjs
|
|
67
|
+
엣지: `shows`(step→screen, 파생이 만든다), `calls`(screen→api), `invokes`(api→function), `touches`(function→table), `covers`(test→screen|api|function), `changes`(commit→screen|api|migration), `defines`(task→decision), `contains`(migration→table|function, release→milestone), `tracks`(milestone→task). 새 종류가 필요하면 엔진 저장소의 `src/lib/graph.mjs` 목록에 더한다(minor 릴리스). 화면이 그 종류를 그리려면 `src/derive.mjs`와 화면 원본 `ui/`도 손봐야 하므로, 먼저 기존 종류로 표현할 수 없는지 본다.
|
|
49
68
|
|
|
50
69
|
## 순서
|
|
51
70
|
|
package/docs/hosting-and-csp.md
CHANGED
|
@@ -58,7 +58,16 @@ async function serveMap(res, sub) {
|
|
|
58
58
|
- Google Fonts 등 외부 스타일·폰트
|
|
59
59
|
- `data:` 이미지
|
|
60
60
|
|
|
61
|
-
엔진
|
|
61
|
+
엔진 서빙(`serve`)이 거는 정책은 `default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'`다. 엔진 화면은 이 정책 아래에서 뜨도록 만들고, 로컬 serve와 예산 검사가 같은 정책으로 확인한다. 배포 서버 정책은 이것과 같거나 더 느슨해야 한다(`data/*.json`을 읽으므로 `connect-src 'self'`가 필요하다).
|
|
62
|
+
|
|
63
|
+
엔진 화면은 그래서 `index.html`(`<div id="root">`와 같은 출처 `map.css`·`map.js`만) + `map.css` + `map.js`다. 1.1.0부터 화면 원본은 엔진 저장소 `ui/`의 React이고, esbuild가 이 세 파일로 번들한다. 파일 이름·위치는 1.0.1과 같다.
|
|
64
|
+
|
|
65
|
+
- React는 브라우저에서만 렌더한다(`createRoot`). 크기·위치 같은 동적 값은 React `style` prop으로 주는데, React는 이것을 `element.style`(CSSOM)로 넣으므로 `style-src 'self'`가 막지 않는다. 마크업에 `style="…"` 속성으로 남지 않는다.
|
|
66
|
+
- 서버 렌더·미리 렌더한 HTML은 쓰지 않는다. 렌더 결과가 마크업의 `style` 속성으로 남아 CSP 위반이 된다.
|
|
67
|
+
- `<style>` 요소를 주입하는 CSS-in-JS·컴포넌트 라이브러리, 외부 서체, `data:` 이미지, `eval`을 쓰지 않는다. 그림은 SVG 속성과 `map.css` 클래스, 애니메이션은 `map.css`의 `@keyframes`, 캡처는 같은 출처 `captures/<id>.jpg`다.
|
|
68
|
+
- 번들 끝에 React 라이선스 고지가 있다(`legalComments: 'eof'`).
|
|
69
|
+
|
|
70
|
+
화면을 고칠 때 이 규칙을 지키면 위 정책에서 뜬다.
|
|
62
71
|
|
|
63
72
|
## CI
|
|
64
73
|
|
|
@@ -95,4 +104,19 @@ Dockerfile에는 `COPY map/.out/site/ ./map/site/` 한 줄. 생성기·어댑터
|
|
|
95
104
|
|
|
96
105
|
## 배포 뒤 검증
|
|
97
106
|
|
|
98
|
-
상태 코드 200은 렌더를 증명하지 않는다. Chromium으로 열어
|
|
107
|
+
상태 코드 200은 렌더를 증명하지 않는다. 배포한 `/map/`을 Chromium으로 열어 다음을 본다.
|
|
108
|
+
|
|
109
|
+
| 확인 | 기대 |
|
|
110
|
+
|---|---|
|
|
111
|
+
| 셸 | 상단 바(`.top`)와 내비 링크 5개가 보인다 |
|
|
112
|
+
| 스타일 적용 | `body` 계산 배경색이 `rgb(5, 7, 10)`이다. 투명이면 `map.css`가 막혔거나 경로가 틀렸다 |
|
|
113
|
+
| 패널 | `.panel` 8개 |
|
|
114
|
+
| 콘솔 | 오류 0, CSP 위반(`securitypolicyviolation`) 0 |
|
|
115
|
+
| 서체 | `fonts/PretendardVariable.woff2` 응답 200 |
|
|
116
|
+
| 자료 | `data/overview.json` 응답 200, `cache-control: no-store` |
|
|
117
|
+
|
|
118
|
+
상황판은 보통 인증 게이트 뒤에 있어 검증 스크립트가 로그인 화면에 막힌다. 게이트를 우회하는 설정을 서버에 더하지 말고, 서버 자체에 바로 붙는 경로를 쓴다.
|
|
119
|
+
|
|
120
|
+
- 게이트 앞단을 거치지 않는 포트포워드(클러스터면 서비스·파드로, 단독 서버면 SSH 터널)로 앱 서버 포트를 로컬에 연다.
|
|
121
|
+
- 앱 서버가 Host 헤더로 경로나 CSP를 나누면 공개 도메인 Host가 필요하다. Chromium은 `extraHTTPHeaders`의 Host를 거부하므로, 브라우저는 공개 주소를 열게 두고 `page.route('**/*', …)`로 요청을 로컬 포트로 바꿔 태우거나 Host를 공개 도메인으로 바꿔 주는 loopback 프록시를 둔다.
|
|
122
|
+
- 이렇게 확인한 것은 서버와 export 폴더다. 게이트 설정 자체(로그인 뒤 `/map/` 접근)는 사람이 브라우저로 한 번 연다.
|
package/docs/migrate.md
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
4. `config.json`에 `"engine": 1`을 더한다.
|
|
13
13
|
5. npm 스크립트를 `livemap` 명령으로 바꾼다(`livemap init`이 없는 스크립트만 넣고 값이 다른 스크립트는 보고한다). 어댑터 단위 검사 스크립트는 지운다. 참조 어댑터 검사는 엔진 저장소 CI가 돈다.
|
|
14
14
|
6. 배포: 이미지 빌드 전에 `npm run map` → `npm run map:export`, 서버는 export 폴더 한 루트를 `/map/`로 준다(`hosting-and-csp.md`).
|
|
15
|
-
7. 확인: 전환
|
|
15
|
+
7. 확인: 전환 전(옛 설치)과 전환 뒤(패키지)를 같은 커밋에서 연속으로 돌려 비교한다.
|
|
16
|
+
- 비교 대상은 `map/.out/`의 `graph.json`·`data.json`·`overview.json`과 `npm run map:check` 출력이다. 전환 전 결과를 다른 폴더에 복사해 두고 전환 뒤 같은 명령으로 다시 만든다.
|
|
17
|
+
- 옛 설치와 같은 판의 엔진으로 옮기면 세 파일이 `generatedAt`을 빼면 같고 `check` 출력도 같다.
|
|
18
|
+
- 전환하면서 엔진 판도 올리면 파일이 같지 않다. minor 판은 필드·노드를 더하기만 하므로, 전환 전 파일의 모든 경로·값이 전환 뒤 파일에 있는지(포함) 보고 새로 생긴 경로가 그 판의 CHANGELOG 항목인지 확인한다. `check`는 전환 전 출력에 CHANGELOG가 적은 새 줄만 더해져야 하고 오류 수는 같아야 한다.
|
|
19
|
+
- 활동 숫자는 실행 시각 기준 14일 창(`git.sinceDays`)이라 두 실행 사이에 창 경계를 넘는 커밋이 있으면 달라진다. 차이가 경계 커밋뿐인지 보고 연속으로 다시 돌린다. `npm ci`와 설치 시간은 두 실행 사이가 아니라 앞에 둔다.
|
|
20
|
+
- 끝으로 `npm run map:budget`이 통과하고 스크린샷 `map/.out/overview-1440.png`이 전환 전과 같은 정보를 보이는지 본다.
|
|
16
21
|
|
|
17
22
|
로컬에서 고친 엔진을 끼워 볼 때는 `npm install --no-save --install-links <엔진 저장소 경로>`를 쓴다. `--install-links` 없이 폴더를 설치하면 심링크가 되어 예산 설정이 `@playwright/test`를 엔진 저장소 쪽에서 찾다 실패한다.
|
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
# 여정 파일 작성
|
|
2
2
|
|
|
3
|
-
`map/semantic/journeys.json
|
|
3
|
+
`map/semantic/journeys.json`(설정 `semantic`)은 제품의 뜻을 사람이 적는 파일이다. 스토리 맵의 backbone(배우가 하는 활동을 순서대로)과 같다. 코드가 아니라 제품의 뜻을 적는 자리이므로 사용자 어휘만 쓰고 라우트·파일명은 `screens`·`capture` 필드에만 둔다.
|
|
4
|
+
|
|
5
|
+
사람이 적는 곳은 넷이고 나머지는 생성기가 저장소를 스캔해 만든다.
|
|
6
|
+
|
|
7
|
+
| 파일 | 적는 것 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| 여정 파일(`semantic`) | 배우·목표·장면과 장면 상태 |
|
|
10
|
+
| 로드맵(`roadmap.file`) | 로드맵 항목과 마일스톤(아래 「로드맵과 마일스톤」) |
|
|
11
|
+
| 작업 장부(`tasks.index`) | 지금 실행 중·대기 중인 작업 |
|
|
12
|
+
| 작업 폴더 문서(`tasks.dir`) | 스펙·계획의 결정(DEC)·계획 항목(PN)·열린 질문 |
|
|
13
|
+
|
|
14
|
+
## 화면 어휘
|
|
15
|
+
|
|
16
|
+
1.1.0 화면은 외부 독자에게 낯선 말을 바꿔 보인다. 파일의 키·값과 이 문서의 작성 어휘는 그대로다.
|
|
17
|
+
|
|
18
|
+
| 여정 파일 | 화면 |
|
|
19
|
+
|---|---|
|
|
20
|
+
| journey(여정) | 기능 |
|
|
21
|
+
| step(장면) | 단계 |
|
|
22
|
+
| `live` | 동작 |
|
|
23
|
+
| `mock` | 목업 |
|
|
24
|
+
| `planned` | 계획 |
|
|
25
|
+
| `next` | 구상 |
|
|
26
|
+
|
|
27
|
+
더보기 > 이 상황판은 이 파일을 "기능 정본 파일"로, 로드맵 파일을 "로드맵 정본"으로 부르고 설정한 경로를 함께 보인다.
|
|
28
|
+
|
|
29
|
+
엔진은 프로젝트가 적은 문구(여정 제목, 레인 이름, 장면 이름 등)를 고치지 않는다. 레인 이름에 "여정"이 들어 있으면 화면에도 그대로 나온다.
|
|
4
30
|
|
|
5
31
|
## 스키마
|
|
6
32
|
|
|
@@ -42,6 +68,7 @@
|
|
|
42
68
|
- `refs` — `DEC-nn`·`PN-nn`(tasks 스펙 문서에서 정의된 것으로 자동 연결), 위키 결정 slug(`decisions/<slug>.md`). 해결되지 않는 DEC/PN 참조는 오류다.
|
|
43
69
|
- `reviewedAt` — 사람이 이 장면을 마지막으로 확인한 날. 그 뒤 화면 파일이 바뀌면 "확인 필요" 경고가 뜬다. 상태를 바꿀 때 같이 갱신한다.
|
|
44
70
|
- `actor` — 장면 단위 배우가 여정 배우와 다를 때만(예: 예약 여정 안의 "리조트 접수함").
|
|
71
|
+
- `actors` — 배우 사전. 여정·장면의 `actor`는 이 사전의 키를 적고, 화면은 값(이름)을 보인다. 사전이 있는데 여정 `actor`나 여정과 다른 장면 `actor`가 키에 없으면 `check`가 경고한다(1.1.0부터, `△ <여정 제목>: 배우 사전에 없는 값 <값>` 또는 `△ <여정 제목> › <장면 이름>: 배우 사전에 없는 값 <값>`). 사전이 없으면 검사하지 않는다. 경고라 종료 코드는 바뀌지 않는다. 사전에 키를 더하거나 `actor`를 사전 키로 고친다.
|
|
45
72
|
|
|
46
73
|
## 인터뷰 질문
|
|
47
74
|
|
|
@@ -58,7 +85,7 @@
|
|
|
58
85
|
## 규칙
|
|
59
86
|
|
|
60
87
|
- 장면 id는 여정 안에서 유일해야 한다(check 오류). intent는 `next`가 아니면 비우지 않는다(check 경고).
|
|
61
|
-
-
|
|
88
|
+
- 개요 기능 지도에는 여정이 12개까지 올라간다. 13개부터는 미완성 여정(파일 순서), 현재 마일스톤의 완성 여정, 나머지 완성 여정(14일 커밋 많은 순) 순서로 12개를 올리고 남은 완성 여정은 지도에서 접는다. 접힌 여정도 기능 화면에는 전부 보인다. 미완성 여정만 13개를 넘으면 앞 12개만 지도에 오르므로, 그때는 여정 하나가 목표 하나인지 다시 본다.
|
|
62
89
|
- 장면 이름은 명사구 2~5어절, intent는 한 문장. 시스템 말투("데이터를 조회한다")가 아니라 사용자 말투("오늘 누가 오는지 본다").
|
|
63
90
|
- 여정에 없는 화면은 "미분류"로 센다. 화면을 어딘가 억지로 넣기보다, 정말 어느 여정에도 안 속하면 그 화면이 필요한지 묻는다.
|
|
64
91
|
- `next`는 스펙도 없는 것이다. 스펙이 생기면 `planned`, 화면이 생기면 `mock`, 실데이터가 붙으면 `live`. 각 전이는 병합과 같은 커밋에서 적는다.
|
|
@@ -75,3 +102,89 @@
|
|
|
75
102
|
| A | 그 검사가 main 최신 커밋에서 통과(`npm run test:report`) |
|
|
76
103
|
|
|
77
104
|
`live`라고 적었는데 D가 나오면 주장이 틀린 것이다. 상태를 `mock`으로 내리거나 화면을 고친다. 상황판은 둘 중 무엇이 맞는지 판단하지 않고 어긋남만 보여준다.
|
|
105
|
+
|
|
106
|
+
## 로드맵과 마일스톤
|
|
107
|
+
|
|
108
|
+
설정 `roadmap.file`(기본 `tasks/roadmap.md`)은 무엇을 어떤 순서로 만드는지 적는 파일이다. `## ` 절 하나가 로드맵 항목 하나이고, 절의 첫 문단이 목표, `- 키: 값` 줄이 속성이다. 파일 안 순서가 항목 순서다. 모르는 키 줄은 무시한다.
|
|
109
|
+
|
|
110
|
+
```markdown
|
|
111
|
+
## 예약금 결제
|
|
112
|
+
|
|
113
|
+
견적을 예약으로 바꾸고 예약금까지 내서 확정한다.
|
|
114
|
+
|
|
115
|
+
- id: deposit
|
|
116
|
+
- 상태: 진행
|
|
117
|
+
- 진행 방식: 스펙 주도
|
|
118
|
+
- 작업: 20260901-deposit
|
|
119
|
+
- 장면: booking/request, booking/pay
|
|
120
|
+
- 선행: search
|
|
121
|
+
- 결정 대기: 사용자: 환불 규칙을 먼저 정할지
|
|
122
|
+
- 완료 기준: 테스트 결제가 끝까지 통과한다
|
|
123
|
+
- 마일스톤: first-release
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
| 키 | 값 |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `id` | 영문 소문자·숫자·`-`. 없으면 항목 순서로 `m{n}`이 붙으니 적어 둔다 |
|
|
129
|
+
| `상태` | 완료·진행·다음·대기·이후. 그 밖이면 check 오류 |
|
|
130
|
+
| `진행 방식` | 자유 문장(예: 스펙 주도) |
|
|
131
|
+
| `작업` | 작업 폴더 이름, 쉼표로 여럿. 없는 폴더는 check 오류 |
|
|
132
|
+
| `장면` | `<여정 id>/<장면 id>`, 쉼표로 여럿. 없는 장면은 check 오류 |
|
|
133
|
+
| `선행` | 먼저 끝나야 하는 항목 id, 쉼표로 여럿. 없는 id는 check 오류, 진행 항목의 선행이 완료가 아니면 경고(1.1.0부터) |
|
|
134
|
+
| `결정 대기` | 이 항목을 멈추게 하는 결정. 자유 문장 또는 `<주체>: <질문>` |
|
|
135
|
+
| `완료 기준` | 자유 문장 |
|
|
136
|
+
| `마일스톤` | 이 항목이 속한 마일스톤 `id` 하나(1.1.0부터). 없으면 미배정 |
|
|
137
|
+
|
|
138
|
+
엔진은 로드맵 파일의 git 이력에서 항목이 완료로 바뀐 마지막 커밋 시각(완료일)과 결정 대기가 적힌 구간의 첫 커밋 시각(결정 대기 시작일)을 계산한다. 날짜 키를 따로 적지 않는다. 커밋하지 않은 변경은 날짜가 없고, git이 없거나 얕은 클론이면 날짜를 계산하지 않는다(얕은 클론이면 로드맵 어댑터가 partial). 파일 이름을 바꿔도 이력을 따라간다.
|
|
139
|
+
|
|
140
|
+
### 결정 대기의 주체
|
|
141
|
+
|
|
142
|
+
`결정 대기` 값이 `<주체>: <질문>` 꼴이면 엔진이 주체와 질문을 나눠 화면에 "사용자 결정 대기 3일째"처럼 보인다. 주체는 콜론이 없는 1~20자이고 콜론(반각·전각) 뒤에 공백이 있어야 한다. 그래서 `https://…`나 `10:30 회의`는 주체로 읽지 않고, 콜론이 없는 문장은 주체 미지정이다.
|
|
143
|
+
|
|
144
|
+
주의: 형식과 상관없이 앞 20자 안에 콜론과 공백이 있으면 앞말이 주체로 읽힌다. "주의: 환불 규칙 확인"은 주체가 "주의"가 된다. 주체를 적지 않을 문장에는 콜론 대신 다른 부호를 쓴다.
|
|
145
|
+
|
|
146
|
+
### 마일스톤
|
|
147
|
+
|
|
148
|
+
마일스톤은 로드맵 항목을 묶어 한 번에 내놓는 릴리스 단위다. 개요는 현재 마일스톤의 진척을 가장 크게 보이고 끝난 마일스톤을 접는다. 같은 로드맵 파일 안에 제목이 `마일스톤:`으로 시작하는 `## ` 절로 적는다. 새 설정 키는 없다.
|
|
149
|
+
|
|
150
|
+
```markdown
|
|
151
|
+
## 마일스톤: 첫 공개
|
|
152
|
+
|
|
153
|
+
새 계정만으로 검색·가입·예약 요청·예약금 테스트 결제가 이어진다.
|
|
154
|
+
|
|
155
|
+
- id: first-release
|
|
156
|
+
- 상태: 진행
|
|
157
|
+
- 목표일: 2026-10-31
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| 키 | 값 | 필요 |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| 제목 | `## 마일스톤: <제목>`(반각·전각 콜론) | 필수 |
|
|
163
|
+
| 첫 문단 | 목표 한 문장 | 선택 |
|
|
164
|
+
| `id` | 영문 소문자·숫자·`-`. 로드맵 항목 id와 겹치면 안 된다 | 필수 |
|
|
165
|
+
| `상태` | 완료·진행·다음·대기·이후(로드맵 항목과 같은 다섯 값) | 필수 |
|
|
166
|
+
| `완료일` | `YYYY-MM-DD` | 상태가 완료면 적는다 |
|
|
167
|
+
| `목표일` | `YYYY-MM-DD` | 선택 |
|
|
168
|
+
| `결정 대기` | 자유 문장 또는 `<주체>: <질문>` | 선택 |
|
|
169
|
+
|
|
170
|
+
- 소속은 항목 쪽 `- 마일스톤: <id>` 키 하나로 정한다. 한 항목은 한 마일스톤에만 속한다.
|
|
171
|
+
- 마일스톤 순서는 파일 안 마일스톤 절 순서다. 항목 순서와 자동 id는 항목 절만 세므로, 마일스톤 절을 끼워도 기존 항목의 순서·id가 바뀌지 않는다. 마일스톤 절은 파일 머리나 소속 항목 바로 앞에 둔다.
|
|
172
|
+
- 엔진이 계산하는 것: 소속 항목, 진척(소속 항목 장면 중 동작 수, 소속 항목 작업의 계획 항목 완료 수), 막힘, 결정 대기 시작일, 현재 마일스톤(파일 순서로 상태가 진행인 첫 마일스톤, 없으면 다음인 첫 마일스톤).
|
|
173
|
+
- 마일스톤 절은 엔진 1.1.0 이상에서만 뜻을 가진다. 1.0.x는 이 절을 로드맵 항목으로 읽어 항목 수와 순서가 틀어진다. 엔진을 1.1.0 이상으로 올린 커밋 뒤에 절을 더한다. 항목의 `마일스톤` 키는 1.0.x가 무시하므로 먼저 적어도 된다.
|
|
174
|
+
|
|
175
|
+
마일스톤 절이 있을 때 `check`가 더하는 줄이다. 오류는 종료 코드 1, 경고는 출력만 한다. 마일스톤 절이 없으면 이 줄은 나오지 않는다.
|
|
176
|
+
|
|
177
|
+
| 수준 | 조건 | 출력 |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| 오류 | 마일스톤 절에 `id` 없음 | `마일스톤 {제목}: id 없음` |
|
|
180
|
+
| 오류 | 마일스톤 id 중복, 또는 로드맵 항목 id와 같음 | `마일스톤 id 중복: {id}` |
|
|
181
|
+
| 오류 | 상태가 다섯 값 밖이거나 비어 있음 | `마일스톤 {제목}: 알 수 없는 상태 {값}` |
|
|
182
|
+
| 오류 | `완료일`·`목표일`이 `YYYY-MM-DD`가 아니거나 없는 날짜 | `마일스톤 {제목}: 날짜 형식 {키} {값}` |
|
|
183
|
+
| 오류 | 항목의 `마일스톤` 값이 없는 id | `로드맵 {항목 제목}: 마일스톤 없음 {id}` |
|
|
184
|
+
| 경고 | 완료인데 소속 항목 중 완료 아님 | `마일스톤 {제목}: 완료인데 미완료 항목 {n}` |
|
|
185
|
+
| 경고 | 완료가 아닌데 소속 항목(1개 이상)이 모두 완료 | `마일스톤 {제목}: 항목이 모두 완료인데 상태 {상태}` |
|
|
186
|
+
| 경고 | 다음·대기·이후인데 소속 항목 중 진행 | `마일스톤 {제목}: 진행 항목이 있는데 상태 {상태}` |
|
|
187
|
+
| 경고 | 소속 항목 0 | `마일스톤 {제목}: 묶인 항목 없음` |
|
|
188
|
+
| 경고 | 완료인데 `완료일` 없음, 또는 완료가 아닌데 `완료일` 있음 | `마일스톤 {제목}: 완료일과 상태가 맞지 않음` |
|
|
189
|
+
| 경고 | 진행 마일스톤 2개 이상 | `진행 마일스톤 {n}개: {제목들}` |
|
|
190
|
+
| 경고 | 마일스톤이 있는데 진행 항목에 `마일스톤` 키 없음 | `로드맵 {항목 제목}: 진행인데 마일스톤 없음` |
|
package/docs/semantic-schema.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# 프로젝트 상황판의 시맨틱 레이어
|
|
2
2
|
|
|
3
|
-
상황판은 두 종류의 사실을 하나의 그래프로 잇는다. 사람이 뜻을 붙이는 노드(
|
|
3
|
+
상황판은 두 종류의 사실을 하나의 그래프로 잇는다. 사람이 뜻을 붙이는 노드(여정·단계·결정·로드맵 항목·마일스톤)와 코드에서 긁어내는 노드(화면·API·함수·테이블·검사·커밋)다. 둘이 어긋나면(단계가 가리키는 라우트가 코드에 없음) 화면에 경고로 드러난다.
|
|
4
|
+
|
|
5
|
+
사람이 적는 곳은 넷이다: 여정 파일(`semantic`), 로드맵(`roadmap.file`, 마일스톤 포함), 작업 장부(`tasks.index`), 작업 폴더 문서(`tasks.dir`의 스펙·계획). 나머지는 생성기가 저장소를 스캔해 만든다. 작성 방법은 `semantic-authoring.md`에 있다.
|
|
6
|
+
|
|
7
|
+
화면은 journey를 "기능", step을 "단계"로 부른다. 노드 종류·파일 키는 그대로다.
|
|
4
8
|
|
|
5
9
|
## 노드
|
|
6
10
|
|
|
@@ -12,12 +16,16 @@
|
|
|
12
16
|
| api | 생성(BFF) | method+path | 서버 진입점. 호출하는 DB 함수·Auth |
|
|
13
17
|
| function | 생성(migration) | name | DB 함수. 읽고 쓰는 테이블, BFF 사용 여부 |
|
|
14
18
|
| table | 생성(migration) | schema.name | 저장 구조 |
|
|
19
|
+
| migration | 생성(migration) | file | migration 파일 하나. 만드는 테이블·함수, grant·RLS 수, 마지막 변경 |
|
|
15
20
|
| test | 생성(tests/) | file | 검사 파일. 다루는 라우트·API |
|
|
21
|
+
| testreport | 생성(JUnit 리포트) | last | 마지막 검사 실행의 건수·실패·건너뜀과 그 커밋이 최신인지 |
|
|
16
22
|
| commit | 생성(git) | sha | 최근 변경. 건드린 파일 → 영향받는 화면·여정 |
|
|
17
|
-
| decision | 생성(위키 index) | file | 결정 페이지와 상태(current/proposed/superseded) |
|
|
18
|
-
|
|
|
23
|
+
| decision | 생성(위키 index, 작업 문서) | file 또는 번호 | 위키 결정 페이지와 상태(current/proposed/superseded). 작업 문서의 `- DEC-nn`(스펙 결정)과 `- [ ] PN-nn`(계획 항목, 완료 여부)도 이 종류로 둔다 |
|
|
24
|
+
| task | 생성(작업 폴더 `tasks.dir`) | 폴더 이름 | 작업 하나. 제목·단계(스펙 초안~검증)·상태·결정 수·계획 항목 완료/미완·열린 질문 수 |
|
|
19
25
|
| ledger | 생성(tasks/index.md) | 행 | 지금 실행 중·대기 중인 작업 |
|
|
20
|
-
|
|
|
26
|
+
| deploy | 생성(git·배포 매니페스트) | head, 배포 대상 | 브랜치 머리 커밋, 매니페스트 이미지 태그의 sha와 뒤처진 커밋 수 |
|
|
27
|
+
| milestone | 손(로드맵 `## 제목` 절) | id | 로드맵 항목. 순서·상태·진행 방식·장면·작업·선행·결정 대기·완료 기준·마일스톤, git 이력에서 계산한 완료일(`completedAt`)·결정 대기 시작일(`waitingSince`) |
|
|
28
|
+
| release | 손(로드맵 `## 마일스톤: 제목` 절, 1.1.0부터) | id | 마일스톤. 순서·상태·목표·완료일·목표일·결정 대기와 그 시작일. 1.x 동안의 임시 이름이고 2.0.0에서 로드맵 항목은 `roadmapItem`, 마일스톤은 `milestone`으로 바꾼다 |
|
|
21
29
|
|
|
22
30
|
## 엣지
|
|
23
31
|
|
|
@@ -30,7 +38,9 @@
|
|
|
30
38
|
- test → screen | api (covers): 검사 파일의 `goto('/…')`·`'/api/…'` 문자열
|
|
31
39
|
- commit → screen | api | function (touches): 파일 경로 → 노드(페이지 파일·닫힘·server.mjs·migration)
|
|
32
40
|
- milestone → task (tracks): 로드맵 항목의 `작업`. 장면·선행은 derive에서 해석하고 없으면 check 오류
|
|
33
|
-
-
|
|
41
|
+
- release → milestone (contains): 로드맵 항목의 `마일스톤` 키. 없는 마일스톤 id면 엣지 없이 check 오류
|
|
42
|
+
- task → decision (defines): 작업 문서의 `- DEC-nn`·`- [ ] PN-nn` 줄
|
|
43
|
+
- step → decision (refs): `refs: ["DEC-57", "PN-15", "trust-boundary"]`. derive가 문자열로 해석한다: `DEC-nn`·`PN-nn`은 그 번호를 정의한 작업(defines)으로, 나머지는 위키 결정 slug로 찾는다
|
|
34
44
|
|
|
35
45
|
## 상태 규칙
|
|
36
46
|
|
|
@@ -38,6 +48,12 @@
|
|
|
38
48
|
- screen.fixedVia: 닫힘이 `fixedPattern`(코드에 고정된 표시값)을 읽는 파일. source는 바꾸지 않고 "하드코딩 표시"로 따로 센다.
|
|
39
49
|
- step.status는 손으로 적되 screens의 source와 대조해 어긋나면 경고(live 단계인데 mock 화면 등).
|
|
40
50
|
- journey.status는 단계에서 유도: 전부 live면 live, 하나라도 live면 partial, 아니면 단계 다수 상태.
|
|
51
|
+
- 로드맵 항목 막힘(`blockedBy`): `waiting`(완료 아님 + 결정 대기), `deps`(진행·다음인데 선행 미완), `task`(추적 작업 중 대기). 마일스톤은 자신의 결정 대기가 있거나 소속 비완료 항목 중 막힌 것이 있으면 `blocked`.
|
|
52
|
+
- 현재 마일스톤: 파일 순서로 상태가 진행인 첫 마일스톤, 없으면 다음인 첫 마일스톤, 없으면 없음.
|
|
53
|
+
|
|
54
|
+
## 문제 기록(issues)
|
|
55
|
+
|
|
56
|
+
어댑터가 `g.issue(level, label, message)`로 낸 오류·경고는 노드가 아니라 `graph.json`·`data.json` 최상위 `issues[]`에 `{level, label, message, adapter}`로 남는다(1.1.0부터, `adapter-contract.md`).
|
|
41
57
|
|
|
42
58
|
## 다른 프로젝트에 옮길 때 바꾸는 것
|
|
43
59
|
|