@takagaki/cortex-decisions-viewer 0.12.123 → 0.12.124

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/dist/build.js CHANGED
@@ -118,6 +118,7 @@ const confirmed_1 = require("./confirmed");
118
118
  const minify_1 = require("./minify");
119
119
  const render_1 = require("./render");
120
120
  const ledger_dir_1 = require("./ledger-dir");
121
+ const view_dir_1 = require("./view-dir");
121
122
  /**
122
123
  * この画面を作ったパッケージの版。**package.json を唯一の出どころにする。**
123
124
  * 別に持つと、公開した版と画面に出る版がずれる(しかも気づけない)。
@@ -732,12 +733,18 @@ function orderRecordsByLedgers(records, recordsDir, ctx, nowIso) {
732
733
  * 全部が人になるため。engine の人の手の表がビューアに届く段 5 で差し替える。
733
734
  * 取りこぼしの代償は「確認の一言が出ない」だけ(消せる範囲は変わらない)。
734
735
  *
736
+ * 記憶 v2 の案件で人の手の表の写し(`memory-hands`)が読めたときは、2 の確認の帳簿の代わりに表の `touched` を見る
737
+ * (cortex-engine #1094 鎖 3 V10b・B9a。確認の帳簿は記憶 v2 では書かれなくなる)。表が読めない晩は今の確認の帳簿に倒す。
738
+ *
735
739
  * @param p リポジトリルート相対の path(`Cortex/<区画>/records/…`)。帳簿の鍵は NFC なので NFC でも引く
736
740
  * @param status その記録の `rstatus`(`String()` 済み)
741
+ * @param hands 人の手の表(path → touched)。渡したときは確認の帳簿を見ない
737
742
  */
738
- function humanTouchedOf(p, status, confirmation) {
743
+ function humanTouchedOf(p, status, confirmation, hands = null) {
739
744
  if ((0, confirmed_1.humanOwned)(p, status))
740
745
  return true;
746
+ if (hands)
747
+ return hands.get(p) === true || hands.get(p.normalize("NFC")) === true;
741
748
  const row = confirmation.get(p) ?? confirmation.get(p.normalize("NFC"));
742
749
  return !!row && row.deleted !== true;
743
750
  }
@@ -746,11 +753,11 @@ function humanTouchedOf(p, status, confirmation) {
746
753
  * (今の SiteData の形を保つ)。path は `repoRoot` 起点で作る(帳簿の鍵はリポジトリルート相対)。
747
754
  * 置き換えの逆引き(`attachSupersedes`)より前に呼ぶ。
748
755
  */
749
- function attachHumanTouched(records, recordsDir, repoRoot, confirmation) {
756
+ function attachHumanTouched(records, recordsDir, repoRoot, confirmation, hands = null) {
750
757
  const recordsRel = path.relative(repoRoot, recordsDir).split(path.sep).join("/");
751
758
  for (const r of records) {
752
759
  const p = recordsRel ? `${recordsRel}/${r.fileName}` : r.fileName;
753
- if (humanTouchedOf(p, r.rstatus, confirmation))
760
+ if (humanTouchedOf(p, r.rstatus, confirmation, hands))
754
761
  r.humanTouched = true;
755
762
  }
756
763
  }
@@ -2370,6 +2377,18 @@ async function externalizeBodies(site, outAbs) {
2370
2377
  }
2371
2378
  }
2372
2379
  async function build(opts) {
2380
+ // 合わせの置き場(`--view-dir`)の一時フォルダは、ビルドが落ちても消す
2381
+ const views = {};
2382
+ try {
2383
+ const r = await buildSite(opts, views);
2384
+ return views.counts ? { ...r, viewCounts: views.counts } : r;
2385
+ }
2386
+ finally {
2387
+ if (views.cleanup)
2388
+ await views.cleanup();
2389
+ }
2390
+ }
2391
+ async function buildSite(opts, views) {
2373
2392
  const dirAbs = await resolveDecisionsDir(opts.dir);
2374
2393
  const outAbs = path.resolve(opts.out);
2375
2394
  const exclude = new Set([...DEFAULT_EXCLUDE, ...opts.exclude].map((e) => e.toLowerCase()));
@@ -2399,7 +2418,22 @@ async function build(opts) {
2399
2418
  // 「リンクにしない」に倒れて全滅する**(以前は順序が逆だった)。
2400
2419
  const repoRoot = knowledgeRoot ? path.dirname(knowledgeRoot) : path.dirname(dirAbs);
2401
2420
  // 帳簿 10 本の置き場(既定は `<repoRoot>/.cortex`)。読み口はこの 1 つから読む
2402
- const ledgerDir = (0, ledger_dir_1.ledgerDirOf)(repoRoot, opts.ledgerDir);
2421
+ let ledgerDir = (0, ledger_dir_1.ledgerDirOf)(repoRoot, opts.ledgerDir);
2422
+ // 記憶 v2 の画面用ファイル(cortex-engine #1094 鎖 3 V10b)。checkout の HEAD とそろう封筒だけを差し替えた合わせの置き場を読む
2423
+ // 合わせの置き場を作れなかった回(一時フォルダの失敗など)は今の帳簿の置き場のまま組む(取り出しの失敗で画面を止めない・数は出さない)
2424
+ if (opts.viewDir) {
2425
+ try {
2426
+ const plan = await (0, view_dir_1.planViews)(path.resolve(opts.viewDir), (0, view_dir_1.checkoutGit)(repoRoot), ledgerDir);
2427
+ const merged = await (0, view_dir_1.mergeLedgerDir)(ledgerDir, plan);
2428
+ views.cleanup = merged.cleanup;
2429
+ views.counts = plan.counts;
2430
+ ledgerDir = merged.dir;
2431
+ console.log((0, view_dir_1.viewCountsLine)(plan.counts));
2432
+ }
2433
+ catch (e) {
2434
+ console.warn(`記憶 v2 の画面用ファイルを使えなかったので、今の帳簿から組みます: ${e instanceof Error ? e.message : String(e)}`);
2435
+ }
2436
+ }
2403
2437
  const targetUrls = knowledgeRoot
2404
2438
  ? await buildTargetUrlMap(repoRoot, repo.baseUrl, repo.branch)
2405
2439
  : new Map();
@@ -2427,7 +2461,12 @@ async function build(opts) {
2427
2461
  // 目次と同じ順位(engine #551)。**読めないなら `null`** で、つながり画面の点は今までどおり一律
2428
2462
  const memoryRanking = await (0, ranking_1.readMemoryRanking)(repoRoot, ledgerDir);
2429
2463
  // 人の手の表の写し(engine #1094 q-10)。あれば記憶 v2 の案件で、確認の印とボタンを出さない。**無い案件は今と 1 バイトも同じ**(キーごと無い)
2430
- const memoryV2 = await (0, hands_1.readMemoryV2)(repoRoot, ledgerDir);
2464
+ // 「記憶 v2 の案件か」は 1 本の栓 `memory_v2_writer` の値で決める(cortex-engine #1094 鎖 3 V10b)。橋の間は写しとの OR
2465
+ const memoryHands = await (0, hands_1.readMemoryHands)(repoRoot, ledgerDir);
2466
+ const memoryV2 = (0, hands_1.isMemoryV2)(opts.memoryV2Writer, memoryHands !== null);
2467
+ // 削除の確認の一言の材料(B9a): 記憶 v2 の案件で表が読めたときは表、ほかは今の確認の帳簿
2468
+ // (表が読めた案件は必ず記憶 v2 なので、表そのものを渡せばよい)
2469
+ const touchedBy = memoryHands;
2431
2470
  // 記録の id → リポジトリルート相対のパス(順位のファイルの鍵と突き合わせる。`repoRoot` 起点・cwd 起点にしない)
2432
2471
  const recordRelPaths = new Map();
2433
2472
  const relOf = (dir, fileName) => {
@@ -2447,7 +2486,7 @@ async function build(opts) {
2447
2486
  rawDates.set(rec, rd);
2448
2487
  }
2449
2488
  // 人の手(削除の確認の文面を替える材料・cortex-engine #1094 段 4)。**置き換えより前に・repoRoot で**
2450
- attachHumanTouched(decisions, dirAbs, repoRoot, ledgers.confirmation);
2489
+ attachHumanTouched(decisions, dirAbs, repoRoot, ledgers.confirmation, touchedBy);
2451
2490
  // 置き換えの逆引き(#699 F3)。**置き換えの無い案件では 1 件も付かない**
2452
2491
  attachSupersedes(decisions, (d) => rawDates.get(d) ?? d.fm.date, (d) => relOf(dirAbs, d.fileName));
2453
2492
  let home = null;
@@ -2490,7 +2529,7 @@ async function build(opts) {
2490
2529
  const recordsDir = path.join(knowledgeRoot, e.name, "records");
2491
2530
  const records = await loadGenericRecords(recordsDir, exclude, repo.baseUrl, repo.branch);
2492
2531
  // 人の手(削除の確認の文面を替える材料・cortex-engine #1094 段 4)
2493
- attachHumanTouched(records, recordsDir, repoRoot, ledgers.confirmation);
2532
+ attachHumanTouched(records, recordsDir, repoRoot, ledgers.confirmation, touchedBy);
2494
2533
  // ルールの取り下げ(`retracted_by`・cortex-engine #1094 便 4-2d)。ルールの区画だけ・取り下げの無い案件では何も付かない
2495
2534
  if (e.name === "Rules")
2496
2535
  attachRetractedBy(records, decisions, (d) => rawDates.get(d) ?? d.fm.date);
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const commander_1 = require("commander");
5
+ const promises_1 = require("node:fs/promises");
5
6
  const build_1 = require("./build");
6
7
  const program = new commander_1.Command();
7
8
  program
@@ -23,6 +24,10 @@ program
23
24
  .option("--webhook-url <url>", "Backlog Webhook 受信のLambda URL(環境変数 WEBHOOK_URL でも可)")
24
25
  .option("--webhook-token <token>", "リアルタイム同期の**案件別**トークン(環境変数 WEBHOOK_TOKEN でも可。艦隊共通トークンを渡さないこと)")
25
26
  .option("--ledger-dir <dir>", "ビューアが読む帳簿(.cortex の 10 本)の置き場(未指定なら <リポジトリルート>/.cortex)")
27
+ .option("--view-dir <dir>", "記憶 v2 の画面用ファイル(保管庫の gold/v1/c/view/ を取り出した手元のフォルダ)。checkout とそろう物だけ使い、ほかは帳簿の置き場から読む")
28
+ .option("--memory-v2-writer <state>", "栓 memory_v2_writer の値(off / shadow / on。on だけが記憶 v2 の案件)")
29
+ .option("--generated-at <text>", "生成時刻の表示(未指定ならビルドした時刻 \"YYYY-MM-DD HH:MM UTC\"。2 通りのビルドの突き合わせで揃える口)")
30
+ .option("--view-report <file>", "--view-dir の数(4 分類と目次に無い数)を JSON で書く先")
26
31
  .action(async (dir, opts) => {
27
32
  try {
28
33
  const result = await (0, build_1.build)({
@@ -32,7 +37,7 @@ program
32
37
  repoUrl: opts.repoUrl,
33
38
  branch: opts.branch,
34
39
  exclude: opts.exclude ?? [],
35
- generatedAt: new Date().toISOString().replace("T", " ").slice(0, 16) + " UTC",
40
+ generatedAt: opts.generatedAt ?? new Date().toISOString().replace("T", " ").slice(0, 16) + " UTC",
36
41
  intakeUrl: opts.intakeUrl ?? process.env.INTAKE_URL,
37
42
  intakeToken: opts.intakeToken ?? process.env.INTAKE_TOKEN,
38
43
  projectKey: opts.projectKey,
@@ -40,7 +45,14 @@ program
40
45
  webhookUrl: opts.webhookUrl ?? process.env.WEBHOOK_URL,
41
46
  webhookToken: opts.webhookToken ?? process.env.WEBHOOK_TOKEN,
42
47
  ledgerDir: opts.ledgerDir,
48
+ viewDir: opts.viewDir,
49
+ memoryV2Writer: opts.memoryV2Writer,
43
50
  });
51
+ if (opts.viewReport) {
52
+ // 数だけ(名前・本文は書かない)。--view-dir を渡さなかった回は書かない
53
+ if (result.viewCounts)
54
+ await (0, promises_1.writeFile)(opts.viewReport, `${JSON.stringify(result.viewCounts)}\n`, "utf8");
55
+ }
44
56
  console.log(`✓ ${result.count} 件のレコードを ${result.outDir} にビルドしました`);
45
57
  }
46
58
  catch (err) {
package/dist/hands.js CHANGED
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.MEMORY_HANDS_VERSION = exports.MEMORY_HANDS_FILE = void 0;
7
7
  exports.isMemoryHandsCopy = isMemoryHandsCopy;
8
8
  exports.readMemoryV2 = readMemoryV2;
9
+ exports.readMemoryHands = readMemoryHands;
10
+ exports.isMemoryV2 = isMemoryV2;
9
11
  /**
10
12
  * 人の手の表の写し(`.cortex/memory-hands.json`)を読む(cortex-engine #1094 q-10・仕様 1-6)。
11
13
  *
@@ -38,15 +40,44 @@ function isMemoryHandsCopy(text) {
38
40
  }
39
41
  /** 案件リポに写しがあり、読める形か。無い → `false`(黙る)・壊れている → `false`(build のログに 1 行) */
40
42
  async function readMemoryV2(repoRoot, ledgerDir) {
43
+ return (await readMemoryHands(repoRoot, ledgerDir)) !== null;
44
+ }
45
+ /**
46
+ * 人の手の表の写しの中身(記録の path → 人の手があるか `touched`)。無い・読めない → `null`(壊れているときだけ build のログに 1 行)。
47
+ * 鍵は engine が綴った path(リポジトリルート相対・NFC)。値の欄は `touched` だけを持つ(素通しはしない)
48
+ */
49
+ async function readMemoryHands(repoRoot, ledgerDir) {
41
50
  let text;
42
51
  try {
43
52
  text = await promises_1.default.readFile(node_path_1.default.join((0, ledger_dir_1.ledgerDirOf)(repoRoot, ledgerDir), exports.MEMORY_HANDS_FILE), "utf8");
44
53
  }
45
54
  catch {
46
- return false;
55
+ return null;
47
56
  }
48
- const ok = isMemoryHandsCopy(text);
49
- if (!ok)
57
+ if (!isMemoryHandsCopy(text)) {
50
58
  console.warn(`${exports.MEMORY_HANDS_FILE}: 読めないので、この案件は記憶 v2 でないものとして出します`);
51
- return ok;
59
+ return null;
60
+ }
61
+ const records = JSON.parse(text).records;
62
+ const out = new Map();
63
+ for (const [p, v] of Object.entries(records)) {
64
+ const row = v && typeof v === "object" && !Array.isArray(v) ? v : null;
65
+ // 🔴 厳密に true のときだけ("true" や 1 を人の手と読まない)
66
+ out.set(p, !!row && row.touched === true);
67
+ }
68
+ return out;
69
+ }
70
+ /** 1 本の栓 `memory_v2_writer` の値の語彙(engine `scripts/observe/rollout.mjs` の `ROLLOUT_STATES`) */
71
+ const WRITER_STATES = new Set(["off", "shadow", "on"]);
72
+ /**
73
+ * この案件は記憶 v2 か(cortex-engine #1094 鎖 3 V10b・確定版 3 章 4)。**1 本の栓 `memory_v2_writer` の値で決める**
74
+ * (workflow が SSM の値を `--memory-v2-writer` で渡す・`on` だけが v2。`shadow` は `off` に潰す=engine と同じ)。
75
+ * 橋の間(V11b まで)は、人の手の表の写しがあることとの OR(受け口 `infra/lambda/intake/logic.mjs` の `memoryV2GateOf` と同じ OR)。
76
+ * 渡されない・語彙外の値は「栓は on でない」と読む(写しがあれば v2・今と同じ)
77
+ */
78
+ function isMemoryV2(writer, handsCopy) {
79
+ const w = typeof writer === "string" ? writer.trim() : "";
80
+ if (w && !WRITER_STATES.has(w))
81
+ console.warn(`--memory-v2-writer の値が語彙の外です(off として読みます): ${JSON.stringify(w.slice(0, 20))}`);
82
+ return w === "on" || handsCopy;
52
83
  }
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.VIEW_INDEX_FILE = exports.VIEW_HELD = exports.VIEW_NAMES = void 0;
7
+ exports.blobShaOf = blobShaOf;
8
+ exports.isReadableEnvelope = isReadableEnvelope;
9
+ exports.classifyView = classifyView;
10
+ exports.checkoutGit = checkoutGit;
11
+ exports.planViews = planViews;
12
+ exports.mergeLedgerDir = mergeLedgerDir;
13
+ exports.viewCountsLine = viewCountsLine;
14
+ /**
15
+ * 記憶 v2 の画面用ファイルの読み口 `--view-dir`(cortex-engine #1094 鎖 3 V10b・確定版 3 章 2)。
16
+ *
17
+ * engine の記憶側の晩の終わりの段 `views`(V10a・`scripts/memory-v2/memory/post.mjs`)が、`.cortex` の帳簿 8 本と順位(段 `ranking`)を
18
+ * 封筒 `{ v:1, name, run, at, commit, blob, text }` に包んで保管庫の `gold/v1/c/view/<名前>.json` に置き、最後に目次 `view-index.json`
19
+ * (名前ごとに `{ blob, commit, at, absent }`)を置く。workflow(engine の `build-viewer.yml`)がそれを 1 行の `aws s3 sync` で作業場に
20
+ * 取り出し、このビューアに `--view-dir` で渡す。🔴 **ビューアは保管庫を知らないまま**(ほどくのは手元のファイルだけ・AWS SDK を足さない)。
21
+ *
22
+ * ## 何をするか
23
+ *
24
+ * 目次を読み、名前ごとに封筒をほどいて分類する(`classifyView`・中身で決める):
25
+ * - `same`: 封筒の本文が今の帳簿の置き場のファイルとバイトで同じ(無いの封筒は、置き場にも無い)→ **封筒の本文を使う**
26
+ * - `stale`・`unexplained`・`unverified`・`unreadable`: 中身が違うか読めない → 名前ごとに今の帳簿に倒す(数え分けは `classifyView`)
27
+ * 目次に載っていない名前(目次が無い晩を含む)は `missing` に数え、`.cortex` に倒す(橋の間・B8)。
28
+ *
29
+ * 使う物だけを差し替えた**合わせの置き場**(一時フォルダ)を作り、ビルドはそこを帳簿の置き場として読む(読み口は今の `--ledger-dir` と同じ 1 本)。
30
+ * 封筒の本文が使えるのは、HEAD の `.cortex` とバイトで同じとき(same)だけなので、橋の間は `.cortex` から組んだサイトと 1 バイトも変わらない。
31
+ *
32
+ * ## 名前(engine の宣言表 `publish: "viewer"` の行・V10a の `VIEW_HELD`)
33
+ *
34
+ * - 読む 11 本のうち、記憶側が置くのは 8 本+順位 1 本(`VIEW_NAMES`)
35
+ * - 名寄せの提案(`gold-proposals`・A1 の答えまで)と確認の帳簿(`gold-confirmation`・B9a で人の手の表に寄せる)は置かれないので、いつも `.cortex` から読む
36
+ */
37
+ const node_child_process_1 = require("node:child_process");
38
+ const node_crypto_1 = require("node:crypto");
39
+ const promises_1 = __importDefault(require("node:fs/promises"));
40
+ const node_os_1 = __importDefault(require("node:os"));
41
+ const node_path_1 = __importDefault(require("node:path"));
42
+ const ledger_dir_1 = require("./ledger-dir");
43
+ /** 記憶側が置く名前(宣言表の `publish: "viewer"` の行から、置かない 2 本を除いた 8 本と順位)。並びは名前順 */
44
+ exports.VIEW_NAMES = Object.freeze([
45
+ "gold-activity",
46
+ "gold-change-log",
47
+ "gold-member-engagement",
48
+ "gold-rederivation",
49
+ "gold-term-strength",
50
+ "memory-hands",
51
+ "memory-ranking",
52
+ "mirror-moved-meetings",
53
+ "story-days",
54
+ ]);
55
+ /** 置かれない名前(いつも `.cortex` から読む)と理由 */
56
+ exports.VIEW_HELD = Object.freeze({
57
+ "gold-proposals": "名寄せの提案のカード(A1 の答えまで .cortex)",
58
+ "gold-confirmation": "削除の確認の一言(記憶 v2 の案件では人の手の表に寄せる・B9a)",
59
+ });
60
+ /** 目次の名前(engine `VIEW_INDEX_NAME`) */
61
+ exports.VIEW_INDEX_FILE = "view-index.json";
62
+ const SHA_RE = /^[0-9a-f]{40}$/;
63
+ const isSha = (s) => typeof s === "string" && SHA_RE.test(s);
64
+ /** git の blob の sha(engine `blobShaOf` と同じ・utf8 のバイト列) */
65
+ function blobShaOf(text) {
66
+ const body = Buffer.from(String(text), "utf8");
67
+ return (0, node_crypto_1.createHash)("sha1").update(Buffer.concat([Buffer.from(`blob ${body.length}\0`, "utf8"), body])).digest("hex");
68
+ }
69
+ /** 封筒が読める形か(engine `isReadableEnvelope` の写し) */
70
+ function isReadableEnvelope(e) {
71
+ if (!e || typeof e !== "object" || Array.isArray(e))
72
+ return false;
73
+ const o = e;
74
+ if (o.v !== 1 || typeof o.name !== "string" || typeof o.run !== "string" || typeof o.at !== "string")
75
+ return false;
76
+ if (!isSha(o.commit))
77
+ return false;
78
+ if (o.absent === true)
79
+ return o.blob === null && o.text === null;
80
+ return isSha(o.blob) && typeof o.text === "string";
81
+ }
82
+ /**
83
+ * 1 本の封筒の分類(ビューアの読み手の比べ)。**中身で決める**: 封筒の本文が、今の帳簿の置き場のファイルとバイトで同じなら `same`(使う)。
84
+ *
85
+ * engine の `classifyEnvelope`(`view-publish.mjs`)は「封筒の commit が HEAD の祖先か」を先に見るが、ここはそれを使わない。
86
+ * 記憶側は同じ中身の晩に置き直さない(封筒の commit は古くなる)・ビルドの checkout は浅い(`fetch-depth: 2000`)・
87
+ * push で起きたビルドは公開より前の版を checkout していることがある、の 3 つで、祖先の判じが中身と関係なく落ちるため(実装レビュー 🔴1・🟠2)。
88
+ * git は、中身が違うときに「説明がつくか」を分けるのにだけ使う:
89
+ * - `stale`: 封筒の commit が HEAD の祖先でその版の blob と合う(後から帳簿が変わった)・HEAD が封筒の commit の祖先(checkout が公開より古い)・
90
+ * 目次と封筒が食い違う(取り出しと公開が重なった)
91
+ * - `unexplained`: 名前違い・本文の sha が blob と違う・その版の blob と違う・どちらも祖先でない(枝分かれ)
92
+ * - `unverified`: 中身が違い、封筒の commit が手元の履歴に無い(浅い clone)。門の外の数
93
+ * - `unreadable`: 封筒・目次の行が読めない・形が違う
94
+ */
95
+ function classifyView({ envelope, expectedName, indexRow, currentBlob, head, isAncestor, blobAt, }) {
96
+ if (!indexRow || !isReadableEnvelope(envelope))
97
+ return "unreadable";
98
+ const e = envelope;
99
+ if (e.name !== expectedName)
100
+ return "unexplained";
101
+ if (e.absent !== true && blobShaOf(e.text) !== e.blob)
102
+ return "unexplained";
103
+ const envBlob = e.absent === true ? null : e.blob;
104
+ if (envBlob === currentBlob)
105
+ return "same";
106
+ // 目次と封筒の食い違い(取り出しの sync と公開の Put が重なった)は、後から変わったのと同じに数える
107
+ if (indexRow.commit !== e.commit || indexRow.blob !== e.blob || indexRow.absent !== (e.absent === true))
108
+ return "stale";
109
+ if (head === null)
110
+ return "unverified";
111
+ try {
112
+ if (isAncestor(e.commit, head))
113
+ return (blobAt(e.commit, e.name) ?? null) === envBlob ? "stale" : "unexplained";
114
+ return isAncestor(head, e.commit) ? "stale" : "unexplained";
115
+ }
116
+ catch {
117
+ return "unverified";
118
+ }
119
+ }
120
+ /** checkout の git(`repoRoot` で呼ぶ)。🔴 投げる口はそのまま投げる(比べの側で unexplained に倒す) */
121
+ function checkoutGit(repoRoot) {
122
+ const git = (args) => (0, node_child_process_1.execFileSync)("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
123
+ return {
124
+ head: () => {
125
+ const h = git(["rev-parse", "--verify", "HEAD^{commit}"]);
126
+ if (!isSha(h))
127
+ throw new Error("HEAD を読めません");
128
+ return h;
129
+ },
130
+ isAncestor: (a, b) => {
131
+ if (!isSha(a) || !isSha(b))
132
+ throw new Error("commit の形が違います");
133
+ try {
134
+ (0, node_child_process_1.execFileSync)("git", ["merge-base", "--is-ancestor", a, b], { cwd: repoRoot, stdio: "ignore" });
135
+ return true;
136
+ }
137
+ catch (e) {
138
+ // 1 は「祖先でない」。それ以外(128=commit が無い・浅い clone)は判じられない
139
+ if (e.status === 1)
140
+ return false;
141
+ throw e;
142
+ }
143
+ },
144
+ blobAt: (commit, name) => {
145
+ if (!isSha(commit))
146
+ throw new Error("commit の形が違います");
147
+ try {
148
+ const sha = git(["rev-parse", "--verify", "-q", `${commit}:./${ledger_dir_1.DEFAULT_LEDGER_DIR_NAME}/${name}.json`]);
149
+ return isSha(sha) ? sha : null;
150
+ }
151
+ catch {
152
+ // その版に commit があって、ファイルだけが無い → null。commit そのものが無いのは判じられない(投げる)
153
+ (0, node_child_process_1.execFileSync)("git", ["cat-file", "-e", `${commit}^{commit}`], { cwd: repoRoot, stdio: "ignore" });
154
+ return null;
155
+ }
156
+ },
157
+ };
158
+ }
159
+ async function readText(p) {
160
+ try {
161
+ return await promises_1.default.readFile(p, "utf8");
162
+ }
163
+ catch {
164
+ return null;
165
+ }
166
+ }
167
+ /** 目次を読む。無い → `null`・壊れている → `"unreadable"`・読める → 名前ごとの行 */
168
+ async function readIndex(viewDir) {
169
+ const text = await readText(node_path_1.default.join(viewDir, exports.VIEW_INDEX_FILE));
170
+ if (text === null)
171
+ return null;
172
+ try {
173
+ const doc = JSON.parse(text);
174
+ const o = doc && typeof doc === "object" && !Array.isArray(doc) ? doc : null;
175
+ const names = o && o.v === 1 && o.names && typeof o.names === "object" && !Array.isArray(o.names) ? o.names : null;
176
+ if (!names)
177
+ return "unreadable";
178
+ const out = {};
179
+ for (const [k, v] of Object.entries(names)) {
180
+ const r = v && typeof v === "object" && !Array.isArray(v) ? v : null;
181
+ // 形の違う行は null(その名前は unreadable に数える。missing に紛れさせない・実装レビュー 🟡4)
182
+ if (!r || !isSha(r.commit) || !(r.blob === null || isSha(r.blob))) {
183
+ out[k] = null;
184
+ continue;
185
+ }
186
+ out[k] = { blob: r.blob, commit: r.commit, absent: r.absent === true };
187
+ }
188
+ return out;
189
+ }
190
+ catch {
191
+ return "unreadable";
192
+ }
193
+ }
194
+ /**
195
+ * 画面用ファイルの置き場を読んで、名前ごとに使うかを決める(ファイルは書かない)。`baseDir` は今の帳簿の置き場(`.cortex` か `--ledger-dir`)で、
196
+ * same は「封筒の本文がそのフォルダのファイルとバイトで同じ」(だから合わせの置き場は今の帳簿と 1 バイトも変わらない)。
197
+ * git(HEAD・祖先・その版の blob)は、中身が違うときの数え分けにだけ使う
198
+ */
199
+ async function planViews(viewDir, git, baseDir) {
200
+ const classes = {};
201
+ const use = {};
202
+ const index = await readIndex(viewDir);
203
+ let head = null;
204
+ try {
205
+ head = git.head();
206
+ }
207
+ catch {
208
+ head = null;
209
+ }
210
+ for (const name of exports.VIEW_NAMES) {
211
+ if (index === null || (index !== "unreadable" && !Object.prototype.hasOwnProperty.call(index, name))) {
212
+ classes[name] = "missing";
213
+ continue;
214
+ }
215
+ if (index === "unreadable") {
216
+ classes[name] = "unreadable";
217
+ continue;
218
+ }
219
+ const text = await readText(node_path_1.default.join(viewDir, `${name}.json`));
220
+ let env = null;
221
+ try {
222
+ env = text === null ? null : JSON.parse(text);
223
+ }
224
+ catch {
225
+ env = null;
226
+ }
227
+ const cur = await readText(node_path_1.default.join(baseDir, `${name}.json`));
228
+ const c = classifyView({
229
+ envelope: env,
230
+ expectedName: name,
231
+ indexRow: index[name],
232
+ currentBlob: cur === null ? null : blobShaOf(cur),
233
+ head,
234
+ isAncestor: git.isAncestor,
235
+ blobAt: git.blobAt,
236
+ });
237
+ classes[name] = c;
238
+ if (c === "same")
239
+ use[name] = env.absent === true ? null : env.text;
240
+ }
241
+ const counts = { total: exports.VIEW_NAMES.length, same: 0, stale: 0, unexplained: 0, unreadable: 0, unverified: 0, missing: 0 };
242
+ for (const c of Object.values(classes))
243
+ counts[c] += 1;
244
+ return { classes, counts, use };
245
+ }
246
+ /**
247
+ * 合わせの置き場を作る: `baseDir`(`.cortex` か `--ledger-dir`)の中身を一時フォルダに写し、same の名前だけ封筒の本文で差し替える
248
+ * (「無い」の封筒はその名前を消す)。戻りの `cleanup` で消す
249
+ */
250
+ async function mergeLedgerDir(baseDir, plan) {
251
+ const dir = await promises_1.default.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), "cortex-view-"));
252
+ try {
253
+ await promises_1.default.cp(baseDir, dir, { recursive: true });
254
+ }
255
+ catch (e) {
256
+ if (e?.code !== "ENOENT") {
257
+ await promises_1.default.rm(dir, { recursive: true, force: true });
258
+ throw e;
259
+ }
260
+ }
261
+ for (const [name, text] of Object.entries(plan.use)) {
262
+ const p = node_path_1.default.join(dir, `${name}.json`);
263
+ if (text === null)
264
+ await promises_1.default.rm(p, { force: true });
265
+ else
266
+ await promises_1.default.writeFile(p, text, "utf8");
267
+ }
268
+ return { dir, cleanup: () => promises_1.default.rm(dir, { recursive: true, force: true }) };
269
+ }
270
+ /** 数の 1 行(ビルドのログ。名前は出さない) */
271
+ function viewCountsLine(c) {
272
+ return `記憶 v2 の画面用ファイル: ${c.total} 本のうち 使った ${c.same}・後から変わった ${c.stale}・説明のつかない ${c.unexplained}・読めない ${c.unreadable}・履歴で確かめられない ${c.unverified}・目次に無い ${c.missing}(使わなかった分は今の帳簿から読みました)`;
273
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.123",
3
+ "version": "0.12.124",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,