@takagaki/cortex-decisions-viewer 0.12.167 → 0.12.172

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
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.RELATED_MATERIALS_SECTIONS = exports.RELATED_URL_MAX = exports.RELATED_MATERIALS_FIELD = exports.DECISION_FIRMNESS = exports.GRAPH_RELS = exports.GRAPH_RECORD_TYPES = exports.PROJECT_ICON_EXTS = exports.MIRROR_RELS = void 0;
39
+ exports.REFERENCES_SECTIONS = exports.RELATED_MATERIALS_SECTIONS = exports.RELATED_URL_MAX = exports.RELATED_MATERIALS_FIELD = exports.DECISION_FIRMNESS = exports.GRAPH_RELS = exports.GRAPH_RECORD_TYPES = exports.PROJECT_ICON_EXTS = exports.MIRROR_RELS = void 0;
40
40
  exports.sanitizeBody = sanitizeBody;
41
41
  exports.findProjectIcon = findProjectIcon;
42
42
  exports.readFleetSources = readFleetSources;
@@ -52,6 +52,9 @@ exports.attachGlow = attachGlow;
52
52
  exports.bodyFileSlug = bodyFileSlug;
53
53
  exports.bodyStampId = bodyStampId;
54
54
  exports.bodyStampComment = bodyStampComment;
55
+ exports.evidenceKeyer = evidenceKeyer;
56
+ exports.evidenceRowsOf = evidenceRowsOf;
57
+ exports.evidencePayloads = evidencePayloads;
55
58
  exports.build = build;
56
59
  exports.decisionFirmnessOf = decisionFirmnessOf;
57
60
  exports.decisionStrandOf = decisionStrandOf;
@@ -59,7 +62,7 @@ exports.decidedByOf = decidedByOf;
59
62
  exports.isRelatedUrl = isRelatedUrl;
60
63
  exports.relatedMaterialsRead = relatedMaterialsRead;
61
64
  exports.relatedMaterialsView = relatedMaterialsView;
62
- exports.attachRuleReferences = attachRuleReferences;
65
+ exports.attachRecordReferences = attachRecordReferences;
63
66
  exports.attachRelatedMaterials = attachRelatedMaterials;
64
67
  exports.createRefResolver = createRefResolver;
65
68
  const node_crypto_1 = require("node:crypto");
@@ -75,6 +78,7 @@ const repo_1 = require("./repo");
75
78
  const engagement_1 = require("./engagement");
76
79
  const story_1 = require("./story");
77
80
  const ranking_1 = require("./ranking");
81
+ const memory_evidence_1 = require("./memory-evidence");
78
82
  const hands_1 = require("./hands");
79
83
  const rederivation_1 = require("./rederivation");
80
84
  const external_id_1 = require("./external-id");
@@ -1840,7 +1844,12 @@ function buildGraph(decisions, sections, targetUrls, repoBaseUrl, book = (0, evi
1840
1844
  if (!nodes.has(r.id))
1841
1845
  continue; // 点にしない種類(Home・レポート・その他)の線は引かない
1842
1846
  addRelationEdges(r.id, r.relations);
1843
- addSourceEdge(r.id, r.source);
1847
+ // 根拠(references・便 B3)。用語・関係者の移行前の source は references の先頭に入っているので、同じ値の source の線は引かない
1848
+ if (r.references)
1849
+ addReferenceEdges(r.id, r.references);
1850
+ const src = r.source ? r.source.trim() : "";
1851
+ if (src && !(r.references ?? []).some((ref) => REF_RAW.get(ref) === src))
1852
+ addSourceEdge(r.id, r.source);
1844
1853
  }
1845
1854
  }
1846
1855
  // 自己ループと重複辺を除去する(同じURLが複数回貼られていても辺は1本)
@@ -2167,6 +2176,148 @@ async function externalizeBodies(site, outAbs) {
2167
2176
  await write(rec, ref, bodyStampId(kind, rec.id, dup ? originOf(rec) : undefined));
2168
2177
  }
2169
2178
  }
2179
+ /** 区画ごとに画面に出す項目の種類 */
2180
+ const EVIDENCE_KINDS_OF = {
2181
+ Decisions: ["decided", "restated"],
2182
+ Rules: ["ruled"],
2183
+ Glossary: ["created", "rewrote"],
2184
+ Members: ["created", "rewrote"],
2185
+ };
2186
+ /** 照らしの鍵を作る口(`identifyEvidence` の鍵 → 正規化した URL → 文字) */
2187
+ function evidenceKeyer(ctx) {
2188
+ const idCtx = { book: ctx.book, targetUrls: ctx.targetUrls, dropLink: (u) => u ?? undefined };
2189
+ return (raw) => {
2190
+ const v = String(raw ?? "").trim();
2191
+ const ev = identifyEvidence(v, idCtx);
2192
+ if (ev)
2193
+ return ev.kind === "url" && /^https?:\/\//i.test(ev.id) ? (0, canonical_evidence_1.syntaxNormalizedUrl)(ev.id) ?? ev.id : ev.id;
2194
+ return `text:${v}`;
2195
+ };
2196
+ }
2197
+ /** references 1 本の照らしの鍵(書かれていた値から。無ければ飛び先か名前) */
2198
+ function referenceKey(ref, keyOf) {
2199
+ return keyOf(REF_RAW.get(ref) ?? ref.url ?? ref.label);
2200
+ }
2201
+ /** 台帳の項目 → 画面の行(区画の種類だけ・同じ鍵は 1 行にまとめる)。並べ方は区画で決める */
2202
+ function evidenceRowsOf(section, items, keyOf, refs) {
2203
+ const kinds = EVIDENCE_KINDS_OF[section] ?? [];
2204
+ const rows = [];
2205
+ const byKey = new Map();
2206
+ for (const it of items) {
2207
+ if (!kinds.includes(it.kind))
2208
+ continue;
2209
+ const key = it.url ? keyOf(it.url) : `text:${it.text_ref}`;
2210
+ // 言い直し(restated)は決めた場面(decided)と同じ根拠でも 1 行に溶かさない(日を言い直しの日に替えない)。照らしの鍵 `key` は同じまま
2211
+ const groupKey = `${it.kind === "restated" ? "restated" : "main"}\u0000${key}`;
2212
+ const quotes = it.quotes.map((q) => (q.context ? { speaker: q.speaker, text: q.text, context: true } : { speaker: q.speaker, text: q.text }));
2213
+ const hit = byKey.get(groupKey);
2214
+ if (hit) {
2215
+ for (const q of quotes) {
2216
+ if (!hit.quotes.some((x) => x.text === q.text && x.speaker === q.speaker && !!x.context === !!q.context))
2217
+ hit.quotes.push(q);
2218
+ }
2219
+ if (it.at > hit.at)
2220
+ hit.at = it.at;
2221
+ continue;
2222
+ }
2223
+ const label = it.url ? refs.resolve(it.url).label || it.url : String(it.text_ref);
2224
+ const row = { at: it.at, kind: it.kind, label, url: it.url, key, quotes };
2225
+ byKey.set(groupKey, row);
2226
+ rows.push(row);
2227
+ }
2228
+ if (section === "Decisions") {
2229
+ // 決めた場面が先、言い直しが後。それぞれ発言の日の古い順
2230
+ const rank = (r) => (r.kind === "decided" ? 0 : 1);
2231
+ return rows.map((r, i) => ({ r, i })).sort((a, b) => rank(a.r) - rank(b.r) || (a.r.at < b.r.at ? -1 : a.r.at > b.r.at ? 1 : a.i - b.i)).map((x) => x.r);
2232
+ }
2233
+ // ルール・用語・関係者: 発言の日の新しい順(同じ日は台帳の並びの後ろ=後から積んだものが先)
2234
+ return rows.map((r, i) => ({ r, i })).sort((a, b) => (a.r.at < b.r.at ? 1 : a.r.at > b.r.at ? -1 : b.i - a.i)).map((x) => x.r);
2235
+ }
2236
+ /**
2237
+ * 記録ごとの根拠のファイルの中身を組む。台帳に画面へ出す項目の無い記録(ルールは元の決定にも無い)には作らない。
2238
+ * 戻り値の鍵は記録の実体(SITE に載る同じオブジェクト)
2239
+ */
2240
+ function evidencePayloads(opts) {
2241
+ const out = new Map();
2242
+ if (!opts.ledger)
2243
+ return out;
2244
+ const lookup = (0, memory_evidence_1.evidenceLookup)(opts.ledger);
2245
+ const rowsFor = (section, id) => {
2246
+ const rec = lookup(id, opts.relPathOf(id));
2247
+ return rec ? evidenceRowsOf(section, rec.items, opts.keyOf, opts.refs) : [];
2248
+ };
2249
+ const refKeysOf = (list) => (list ?? []).map((r) => referenceKey(r, opts.keyOf));
2250
+ const decById = new Map();
2251
+ const decRows = new Map();
2252
+ for (const d of opts.decisions) {
2253
+ if (!decById.has(d.id))
2254
+ decById.set(d.id, d);
2255
+ const items = rowsFor("Decisions", d.id);
2256
+ decRows.set(d, items);
2257
+ if (items.length)
2258
+ out.set(d, { v: 1, items, refKeys: refKeysOf(d.references) });
2259
+ }
2260
+ for (const sec of opts.sections) {
2261
+ if (!EVIDENCE_KINDS_OF[sec.name])
2262
+ continue;
2263
+ for (const r of sec.records) {
2264
+ const items = rowsFor(sec.name, r.id);
2265
+ if (sec.name !== "Rules") {
2266
+ if (items.length)
2267
+ out.set(r, { v: 1, items, refKeys: refKeysOf(r.references) });
2268
+ continue;
2269
+ }
2270
+ const origins = [];
2271
+ for (const rel of r.relations) {
2272
+ if (rel.rel !== "derived_from")
2273
+ continue;
2274
+ const d = decById.get(rel.target.trim());
2275
+ if (!d || origins.some((o) => o.id === d.id))
2276
+ continue;
2277
+ origins.push({ id: d.id, items: decRows.get(d) ?? [], refKeys: refKeysOf(d.references) });
2278
+ }
2279
+ if (items.length || origins.some((o) => o.items.length))
2280
+ out.set(r, { v: 1, items, refKeys: refKeysOf(r.references), origins });
2281
+ }
2282
+ }
2283
+ return out;
2284
+ }
2285
+ /** 記録 1 件の根拠の項目の数(ページに載せる件数・ルールは元の決定の分も数える) */
2286
+ function evidenceCountOf(p) {
2287
+ return p.items.length + (p.origins ?? []).reduce((n, o) => n + o.items.length, 0);
2288
+ }
2289
+ /**
2290
+ * 根拠のファイルを out/evidence/... へ書き出し、記録に evidenceRef・evidenceId・evidenceCount を付ける(本文の外出しと同じ作り)。
2291
+ * 🔴 先に丸ごと消す(台帳から消えた記録の古いファイルを URL 直打ちで読ませない・0 件の回にも消す)
2292
+ */
2293
+ async function externalizeEvidence(site, outAbs, payloads) {
2294
+ await node_fs_1.promises.rm(path.join(outAbs, "evidence"), { recursive: true, force: true });
2295
+ if (!payloads.size)
2296
+ return;
2297
+ const all = [
2298
+ ...site.decisions.map((rec) => ({ rec, kind: "decisions", dir: "evidence/decisions" })),
2299
+ ...site.sections.flatMap((s) => s.records.map((rec) => ({ rec, kind: s.name, dir: `evidence/${bodyFileSlug(s.name)}` }))),
2300
+ ].filter((x) => payloads.has(x.rec));
2301
+ const originOf = (rec) => `${rec.editUrl ?? ""}|${rec.fileName}`;
2302
+ const dupCount = new Map();
2303
+ for (const { rec, dir } of all) {
2304
+ const key = `${dir}/${bodyFileSlug(rec.id)}`.toLowerCase();
2305
+ dupCount.set(key, (dupCount.get(key) ?? 0) + 1);
2306
+ }
2307
+ for (const { rec, kind, dir } of all) {
2308
+ const p = payloads.get(rec);
2309
+ const base = `${dir}/${bodyFileSlug(rec.id)}`;
2310
+ const dup = (dupCount.get(base.toLowerCase()) ?? 0) > 1;
2311
+ const ref = dup ? `${base}-${shortDigest(originOf(rec))}.json` : `${base}.json`;
2312
+ const stampId = bodyStampId(kind, rec.id, dup ? originOf(rec) : undefined);
2313
+ const body = { v: 1, id: stampId, items: p.items, refKeys: p.refKeys, ...(p.origins ? { origins: p.origins } : {}) };
2314
+ await node_fs_1.promises.mkdir(path.dirname(path.join(outAbs, ref)), { recursive: true });
2315
+ await node_fs_1.promises.writeFile(path.join(outAbs, ref), JSON.stringify(body), "utf8");
2316
+ rec.evidenceRef = ref;
2317
+ rec.evidenceId = stampId;
2318
+ rec.evidenceCount = evidenceCountOf(p);
2319
+ }
2320
+ }
2170
2321
  async function build(opts) {
2171
2322
  // 合わせの置き場(`--view-dir`)の一時フォルダは、ビルドが落ちても消す
2172
2323
  const views = {};
@@ -2203,7 +2354,7 @@ async function buildSite(opts, views) {
2203
2354
  // 解決するので、あとから作ると空の表・空の実在判定が渡り、**実在するパスまで
2204
2355
  // 「リンクにしない」に倒れて全滅する**(以前は順序が逆だった)。
2205
2356
  const repoRoot = knowledgeRoot ? path.dirname(knowledgeRoot) : path.dirname(dirAbs);
2206
- // 帳簿 10 本の置き場(既定は `<repoRoot>/.cortex`)。読み口はこの 1 つから読む
2357
+ // 帳簿 9 本の置き場(既定は `<repoRoot>/.cortex`)。読み口はこの 1 つから読む(根拠の台帳はいつも `<repoRoot>/.cortex`)
2207
2358
  let ledgerDir = (0, ledger_dir_1.ledgerDirOf)(repoRoot, opts.ledgerDir);
2208
2359
  // 記憶 v2 の画面用ファイル(cortex-engine #1094 鎖 3 V10b)。checkout の HEAD とそろう封筒だけを差し替えた合わせの置き場を読む
2209
2360
  // 合わせの置き場を作れなかった回(一時フォルダの失敗など)は今の帳簿の置き場のまま組む(取り出しの失敗で画面を止めない・数は出さない)
@@ -2243,6 +2394,8 @@ async function buildSite(opts, views) {
2243
2394
  const engagement = await (0, engagement_1.readEngagementLedger)(repoRoot, ledgerDir);
2244
2395
  // 目次と同じ順位(engine #551)。**読めないなら `null`** で、つながり画面の点は今までどおり一律
2245
2396
  const memoryRanking = await (0, ranking_1.readMemoryRanking)(repoRoot, ledgerDir);
2397
+ // 根拠の台帳(便 B3)。**読めないなら `null`** で、根拠の段は記録に書かれた references のリンクだけ
2398
+ const memoryEvidence = await (0, memory_evidence_1.readMemoryEvidence)(repoRoot); // 案件リポの .cortex から(--ledger-dir に従わない・memory-evidence.ts)
2246
2399
  // 人の手の表の写し(engine #1094 q-10)。あれば記憶 v2 の案件で、確認の印とボタンを出さない。**無い案件は今と 1 バイトも同じ**(キーごと無い)
2247
2400
  // 「記憶 v2 の案件か」は 1 本の栓 `memory_v2_writer` の値で決める(cortex-engine #1094 鎖 3 V10b)。橋の間は写しとの OR
2248
2401
  const memoryHands = await (0, hands_1.readMemoryHands)(repoRoot, ledgerDir);
@@ -2312,8 +2465,8 @@ async function buildSite(opts, views) {
2312
2465
  records = records.filter((r) => !r.relations.some((x) => x.rel === "retracted_by"));
2313
2466
  // 関連の資料(cortex-engine #1094 F8)。ルール・用語だけ・欄の無い記録には何も付かない
2314
2467
  attachRelatedMaterials(e.name, records, refs);
2315
- // ルールの参照(rule.card)。決定と同じ出典リンクにする
2316
- attachRuleReferences(e.name, records, refs);
2468
+ // 根拠(references)。ルール・用語・関係者を決定と同じリンクにする(用語・関係者の移行前の source は先頭の 1 本)
2469
+ attachRecordReferences(e.name, records, refs);
2317
2470
  // 用語集の並び(活性度・強さ)。**帳簿を持たない案件では並びもそのまま**
2318
2471
  const rederived = orderRecordsByLedgers(records, recordsDir, rederive, opts.generatedAt);
2319
2472
  // 関係者だけ、関与の多い順(engine の目次と同じ `order`・便 14b)。用語集の並び
@@ -2353,6 +2506,15 @@ async function buildSite(opts, views) {
2353
2506
  // 期間フィルタ用の月(YYYY-MM)を日付から導出し、降順(新しい順)にする
2354
2507
  const months = uniqueSorted(decisions.map((d) => d.date.slice(0, 7)).filter((m) => /^\d{4}-\d{2}$/.test(m))).reverse();
2355
2508
  const graph = buildGraph(decisions, graphSections, targetUrls, repo.baseUrl, evidenceBook);
2509
+ // 記録ごとの根拠(便 B3)。台帳が読めない・項目が無い記録には作らない(書き出しは下の externalizeEvidence)
2510
+ const evidence = evidencePayloads({
2511
+ decisions,
2512
+ sections,
2513
+ ledger: memoryEvidence,
2514
+ relPathOf: (id) => recordRelPaths.get(id),
2515
+ keyOf: evidenceKeyer({ book: evidenceBook, targetUrls }),
2516
+ refs,
2517
+ });
2356
2518
  computeLayout(graph); // 各ノードに x/y を書き込む(クライアントはこれを初期位置に使いウォームアップ省略)
2357
2519
  // 点の明るさと大きさ(engine #551)。順位のファイルが読めない案件では 1 点も付かない(今までと同じ画面)
2358
2520
  if (memoryRanking)
@@ -2475,6 +2637,8 @@ async function buildSite(opts, views) {
2475
2637
  // 本文HTML(リッチ・かさばる)を bodies/ へ外出しし、inlineデータからは除外する。
2476
2638
  // 一覧・グラフ・検索は inline の searchText(軽量plaintext)で動き、詳細表示時にbodyRefをfetchする。
2477
2639
  await externalizeBodies(site, outAbs);
2640
+ // 根拠(便 B3)も本文と同じく evidence/ へ外出しし、ページには evidenceRef と件数だけ
2641
+ await externalizeEvidence(site, outAbs, evidence);
2478
2642
  // CSSとJSは外部ファイルへ出す。**インラインのままだと配信先で画面が出ない**——
2479
2643
  // AISポータルは /p/<案件>/cm/ に厳格な Content-Security-Policy を返しており、
2480
2644
  // インラインの style / script を実行しない(cortex-tools #301)。
@@ -2644,12 +2808,20 @@ function relatedMaterialsView(fm, refs) {
2644
2808
  }
2645
2809
  /** 関連の資料を持てる汎用の区画(関係者・ストーリーの日・Home の欄は拾わない) */
2646
2810
  exports.RELATED_MATERIALS_SECTIONS = new Set(["Rules", "Glossary"]);
2647
- /** ルールの `references` を決定と同じ出典リンクにして載せる(rule.card)。ルールだけ・欄の無い記録にはキーごと付けない */
2648
- function attachRuleReferences(section, records, refs) {
2649
- if (section !== "Rules")
2811
+ /** 根拠(`references`)を持てる汎用の区画(便 B3・ルールに用語・関係者を足した) */
2812
+ exports.REFERENCES_SECTIONS = new Set(["Rules", "Glossary", "Members"]);
2813
+ /**
2814
+ * 記録の `references` を決定と同じ根拠のリンクにして載せる(rule.card・便 B3 で用語・関係者にも)。
2815
+ * 用語・関係者で `references` が無い移行前の記録は、`source` を先頭の 1 本として読む。欄の無い記録にはキーごと付けない
2816
+ */
2817
+ function attachRecordReferences(section, records, refs) {
2818
+ if (!exports.REFERENCES_SECTIONS.has(section))
2650
2819
  return;
2651
2820
  for (const r of records) {
2652
- const list = toStringArray(r.fm.references).map((x) => refs.resolve(x));
2821
+ let raw = toStringArray(r.fm.references).map((x) => x.trim()).filter(Boolean);
2822
+ if (!raw.length && section !== "Rules")
2823
+ raw = toStringArray(r.fm.source).map((x) => x.trim()).filter(Boolean).slice(0, 1);
2824
+ const list = raw.map((x) => refs.resolve(x));
2653
2825
  if (list.length)
2654
2826
  r.references = list;
2655
2827
  }
package/dist/cli.js CHANGED
@@ -23,7 +23,7 @@ program
23
23
  .option("--secret-kinds <kinds>", "鍵の入力欄を出す連携(カンマ区切り。例: figma,backlog)")
24
24
  .option("--webhook-url <url>", "Backlog Webhook 受信のLambda URL(環境変数 WEBHOOK_URL でも可)")
25
25
  .option("--webhook-token <token>", "リアルタイム同期の**案件別**トークン(環境変数 WEBHOOK_TOKEN でも可。艦隊共通トークンを渡さないこと)")
26
- .option("--ledger-dir <dir>", "ビューアが読む帳簿(.cortex の 10 本)の置き場(未指定なら <リポジトリルート>/.cortex)")
26
+ .option("--ledger-dir <dir>", "ビューアが読む帳簿(.cortex の 9 本・根拠の台帳はいつも <リポジトリルート>/.cortex から読む)の置き場(未指定なら <リポジトリルート>/.cortex)")
27
27
  .option("--view-dir <dir>", "記憶 v2 の画面用ファイル(保管庫の gold/v1/c/view/ を取り出した手元のフォルダ)。checkout とそろう物だけ使い、ほかは帳簿の置き場から読む")
28
28
  .option("--memory-v2-writer <state>", "栓 memory_v2_writer の値(off / shadow / on。on だけが記憶 v2 の案件)")
29
29
  .option("--generated-at <text>", "生成時刻の表示(未指定ならビルドした時刻 \"YYYY-MM-DD HH:MM UTC\"。2 通りのビルドの突き合わせで揃える口)")
@@ -48,8 +48,8 @@ const ledger_dir_1 = require("./ledger-dir");
48
48
  exports.ENGAGEMENT_LEDGER_FILE = "gold-member-engagement.json";
49
49
  /** 帳簿の版(engine `scripts/memory-index.mjs` の `LEDGER_VERSION`。違ったら読まない) */
50
50
  exports.ENGAGEMENT_LEDGER_VERSION = 1;
51
- /** 半減期(日)。engine `weights.mjs` の `HALF_LIFE_DAYS`(正本 6-B「半減期 90 日固定」) */
52
- exports.HALF_LIFE_DAYS = 90;
51
+ /** 関係者の並びの半減期(日)。engine `weights.mjs` の `MEMBER_HALF_LIFE_DAYS`(オーナー決定 cortex-engine #1911 6237507335 の 1「90 日で半分」→「30 日で半分」)。用語の並びは engine 側で 90 日のまま */
52
+ exports.HALF_LIFE_DAYS = 30;
53
53
  /**
54
54
  * 行為 1 件の重み。engine `weights.mjs` の `WEIGHTS` の写し(31 値・**表に無い `kind` は 0**)。
55
55
  *
@@ -108,7 +108,7 @@ exports.weightOf = weightOf;
108
108
  /** 月の中央日(`YYYY-MM` → その月の 15 日 12:00 UTC・engine `monthCenterMs`) */
109
109
  const monthCenterMs = (month) => Date.parse(`${month}-15T12:00:00Z`);
110
110
  /**
111
- * score = Σ_月 Σ_kind w × n × 0.5^(その月の中央日からの経過日数 / 90)(engine `scoreOf` の写し)。
111
+ * score = Σ_月 Σ_kind w × n × 0.5^(その月の中央日からの経過日数 / 30)(engine `scoreOf` の写し)。
112
112
  *
113
113
  * 🔴 **足す順を正本と同じに決め打つ**(月昇順 × `kind` 昇順)。浮動小数の和は順序で最下位
114
114
  * ビットが変わるので、順を変えると同点の判定が正本とずれうる。
@@ -7,7 +7,7 @@ exports.DEFAULT_LEDGER_DIR_NAME = void 0;
7
7
  exports.defaultLedgerDir = defaultLedgerDir;
8
8
  exports.ledgerDirOf = ledgerDirOf;
9
9
  /**
10
- * ビューアが読む帳簿(10 本)の置き場(cortex-engine #1094 便 3-11 の tools(T1))。
10
+ * ビューアが読む帳簿(9 本)の置き場(cortex-engine #1094 便 3-11 の tools(T1))。根拠の台帳(便 B3)は案件リポに置くので、ここを通さず `defaultLedgerDir` から読む(`memory-evidence.ts`)。
11
11
  *
12
12
  * 既定は `<リポジトリルート>/.cortex`(今までと同じ)。`--ledger-dir` で別のフォルダを指せる。
13
13
  * ビューアは保管庫(S3)も栓も知らない(AWS SDK を足さない)。どこから持ってくるかは呼び出し側(workflow)の仕事で、
@@ -0,0 +1,133 @@
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.MEMORY_EVIDENCE_KINDS = exports.MEMORY_EVIDENCE_VERSION = exports.MEMORY_EVIDENCE_FILE = void 0;
7
+ exports.memoryEvidenceItemOf = memoryEvidenceItemOf;
8
+ exports.parseMemoryEvidence = parseMemoryEvidence;
9
+ exports.readMemoryEvidence = readMemoryEvidence;
10
+ exports.evidenceLookup = evidenceLookup;
11
+ /**
12
+ * 根拠の台帳 `.cortex/memory-evidence.json` の読み口(便 B3・cortex-engine #1911)。
13
+ *
14
+ * engine の書き手(決定・ルール・用語・関係者)が、記録を作った・書き換えたきっかけの発言を
15
+ * 記録の id ごとに**追記だけ**で積む。ビューアはここを読み、記録ごとの根拠を別ファイル
16
+ * (`evidence/<鍵>.json`)に書き出して、詳細を開いたときに画面が読む(本文の `bodyRef` と同じ作り)。
17
+ *
18
+ * ## 🔴 無い・壊れている・版が違う案件は「今までと同じ画面」
19
+ *
20
+ * 無ければ `null`(黙る)。壊れている・版が違えば `null` と build のログに 1 行(`ranking.ts` と同じ作法)。
21
+ * どちらも画面は `references` のリンクだけで「根拠」の段を描く。
22
+ *
23
+ * ## 🔴 置き場は案件リポ(`--ledger-dir` に従わない)
24
+ *
25
+ * 引用の文字は案件リポ(git)に置く(オーナー決定 B・engine の宣言表 `viewer-direct`・cortex-engine #1911 のリーダーの答え 6239629719 の 1)。
26
+ * engine は保管庫へ移さず画面用のファイルとしても公開しないので、`--ledger-dir`・`--view-dir` を渡しても、いつも `<repo>/.cortex` から読む。
27
+ *
28
+ * ## 項目ごとの形の検め
29
+ *
30
+ * 画面に出すものは項目ごとに確かめ、形の崩れた項目は捨てる(台帳全体は捨てない)。
31
+ * - `url` は http(s) だけ(`javascript:` などは項目ごと捨てる)。`url` も `text_ref` も無い項目も捨てる
32
+ * - `at` は `YYYY-MM-DD` か null(null は空の日として受ける)・`kind` は語彙の 5 つ
33
+ * - 引用の `text` は空でない文字列(崩れた引用だけ捨て、項目は残す)
34
+ */
35
+ const promises_1 = __importDefault(require("node:fs/promises"));
36
+ const node_path_1 = __importDefault(require("node:path"));
37
+ const ledger_dir_1 = require("./ledger-dir");
38
+ /** 置き場(engine `memory/evidence.mjs` の置き場) */
39
+ exports.MEMORY_EVIDENCE_FILE = "memory-evidence.json";
40
+ /** 形の版(違ったら読まない) */
41
+ exports.MEMORY_EVIDENCE_VERSION = 1;
42
+ /** 項目の種類(決定の場面・言い直し・ルール・用語と関係者の作った晩/書き換えた晩) */
43
+ exports.MEMORY_EVIDENCE_KINDS = ["decided", "restated", "ruled", "created", "rewrote"];
44
+ const DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
45
+ // engine の台帳の検め(`^https?://`)と揃える。空白を含む URL も項目ごと捨てない
46
+ const HTTP_RE = /^https?:\/\/\S/i;
47
+ const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
48
+ const isText = (v) => typeof v === "string" && v.trim() !== "";
49
+ /** 引用 1 つ。崩れていれば null */
50
+ function quoteOf(v) {
51
+ if (!isObj(v) || !isText(v.text))
52
+ return null;
53
+ if (v.speaker != null && typeof v.speaker !== "string")
54
+ return null;
55
+ const q = { speaker: typeof v.speaker === "string" ? v.speaker.trim() : "", text: v.text };
56
+ if (typeof v.material === "string" && v.material)
57
+ q.material = v.material;
58
+ if (typeof v.line === "number" && Number.isFinite(v.line))
59
+ q.line = v.line;
60
+ if (v.context === true)
61
+ q.context = true;
62
+ return q;
63
+ }
64
+ /** 項目 1 つ。崩れていれば null(捨てる) */
65
+ function memoryEvidenceItemOf(v) {
66
+ if (!isObj(v))
67
+ return null;
68
+ if (typeof v.key !== "string")
69
+ return null;
70
+ if (v.at != null && (typeof v.at !== "string" || !DAY_RE.test(v.at)))
71
+ return null;
72
+ if (!exports.MEMORY_EVIDENCE_KINDS.includes(v.kind))
73
+ return null;
74
+ if (v.url != null && (typeof v.url !== "string" || !HTTP_RE.test(v.url)))
75
+ return null;
76
+ if (v.text_ref != null && typeof v.text_ref !== "string")
77
+ return null;
78
+ const url = typeof v.url === "string" ? v.url : null;
79
+ const textRef = isText(v.text_ref) ? v.text_ref.trim() : null;
80
+ if (!url && !textRef)
81
+ return null;
82
+ const quotes = Array.isArray(v.quotes) ? v.quotes.map(quoteOf).filter((q) => q !== null) : [];
83
+ const item = { key: v.key, at: typeof v.at === "string" ? v.at : "", kind: v.kind, url, text_ref: url ? null : textRef, quotes };
84
+ if (typeof v.night === "string" && DAY_RE.test(v.night))
85
+ item.night = v.night;
86
+ if (typeof v.cand === "string" && v.cand)
87
+ item.cand = v.cand;
88
+ return item;
89
+ }
90
+ /** 中身を読む(投げない)。形が違う・版が違う → `null`。崩れた記録・項目は捨てる */
91
+ function parseMemoryEvidence(text) {
92
+ try {
93
+ const parsed = JSON.parse(text);
94
+ if (!isObj(parsed) || parsed.version !== exports.MEMORY_EVIDENCE_VERSION || !isObj(parsed.records))
95
+ return null;
96
+ const out = new Map();
97
+ for (const [id, rec] of Object.entries(parsed.records)) {
98
+ if (!id || !isObj(rec) || !Array.isArray(rec.items))
99
+ continue;
100
+ const items = rec.items.map(memoryEvidenceItemOf).filter((x) => x !== null);
101
+ const entry = { items };
102
+ if (isText(rec.path))
103
+ entry.path = rec.path.trim();
104
+ out.set(id, entry);
105
+ }
106
+ return out;
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ /** 案件リポから読む。無い → `null`(黙る)・壊れている/版違い → `null`(build のログに 1 行) */
113
+ async function readMemoryEvidence(repoRoot) {
114
+ let text;
115
+ try {
116
+ text = await promises_1.default.readFile(node_path_1.default.join((0, ledger_dir_1.defaultLedgerDir)(repoRoot), exports.MEMORY_EVIDENCE_FILE), "utf8");
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ const parsed = parseMemoryEvidence(text);
122
+ if (!parsed)
123
+ console.warn(`${exports.MEMORY_EVIDENCE_FILE}: 読めないので、根拠の段は記録に書かれたリンクだけで出します`);
124
+ return parsed;
125
+ }
126
+ /** 記録に台帳の根拠を結ぶ口: id で引き、無ければ置き場(リポジトリルート相対のパス)で引く */
127
+ function evidenceLookup(ledger) {
128
+ const byPath = new Map();
129
+ for (const rec of ledger.values())
130
+ if (rec.path && !byPath.has(rec.path))
131
+ byPath.set(rec.path, rec);
132
+ return (id, relPath) => ledger.get(id) ?? (relPath ? byPath.get(relPath) ?? null : null);
133
+ }
package/dist/render.js CHANGED
@@ -6041,6 +6041,18 @@ button.ops-card-btn:disabled { opacity: .6; cursor: default; }
6041
6041
  .deciders { display: flex; flex-wrap: wrap; gap: 8px; }
6042
6042
  .refs { margin: 0; padding-left: 18px; }
6043
6043
  .refs li { margin: 4px 0; }
6044
+ /* 根拠の段(便 B3)。引用は小さく、前の発言はさらに控えめに。色は既存のトークンだけ */
6045
+ .evidence-list li[hidden] { display: none; }
6046
+ .evidence-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
6047
+ .evidence-at { font-size: 12px; color: var(--muted); }
6048
+ .evidence-text-ref { color: var(--text); }
6049
+ .evidence-quotes { margin: 4px 0 2px; padding-left: 10px; border-left: 3px solid var(--border); }
6050
+ .evidence-quote { font-size: 13.5px; color: var(--text); line-height: 1.6; margin: 2px 0; white-space: pre-wrap; word-break: break-word; }
6051
+ .evidence-quote.is-context { font-size: 12px; color: var(--muted); }
6052
+ .evidence-context-tag { display: inline-block; margin-right: 6px; padding: 0 6px; font-size: 11px; border: 1px solid var(--border); border-radius: 999px; color: var(--muted); }
6053
+ .evidence-more, .evidence-fold-btn { border: none; background: none; padding: 0 2px; font-size: 12.5px; color: var(--accent); cursor: pointer; }
6054
+ .evidence-fold { list-style: none; }
6055
+ .evidence-origin-tag { display: inline-block; margin-right: 6px; font-size: 12px; color: var(--muted); }
6044
6056
  /* 関連の資料(#1094 F8): 名前の引けない資料は URL のまま(最大 2000 字)なので、狭い画面ではみ出さないよう折り返す */
6045
6057
  .related-materials li { overflow-wrap: anywhere; }
6046
6058
  .body {
@@ -6929,7 +6941,7 @@ ${story_1.splitByNameLinks.toString()}
6929
6941
  */
6930
6942
  /**
6931
6943
  * 詳細の節「関連の資料」(cortex-engine #1094 F8)。記録を生んだ会話に貼られた資料へのリンク。
6932
- * 根拠(「参照」)とは混ぜない。ビルドが engine と同じ門で検めてあるが、画面でも http(s) だけをリンクにする(二重に見る)。
6944
+ * 根拠(「根拠」の節)とは混ぜない。ビルドが engine と同じ門で検めてあるが、画面でも http(s) だけをリンクにする(二重に見る)。
6933
6945
  * 関連の資料の無い記録では null(要素を作らない)
6934
6946
  */
6935
6947
  function relatedMaterialsSection(r) {
@@ -6949,21 +6961,184 @@ ${story_1.splitByNameLinks.toString()}
6949
6961
  return box;
6950
6962
  }
6951
6963
  /**
6952
- * 詳細の節「参照」(根拠の出典リンク)。決定とルールが同じ出し方(rule.card・出典リンク)。
6953
- * 参照の無い記録では null(要素を作らない)。リンクは http(s) だけ(ビルドが解決済みでも画面で二重に見る)
6954
- */
6955
- function referencesSection(r) {
6956
- if (!r || !r.references || !r.references.length) return null;
6957
- var box = el("div", { class: "refs-section" });
6958
- box.appendChild(el("div", { class: "section-label", text: "参照" }));
6959
- var ul = el("ul", { class: "refs" });
6960
- r.references.forEach(function (ref) {
6961
- var li = el("li");
6962
- if (ref.url && /^https?:\\/\\//i.test(ref.url)) li.appendChild(el("a", { href: ref.url, target: "_blank", rel: "noopener", text: ref.label }));
6963
- else li.textContent = ref.label;
6964
- ul.appendChild(li);
6964
+ * 詳細の節「根拠」(便 B3・cortex-engine #1911)。元のツールのリンクと、記録を作った・書き換えたきっかけの発言の引用。
6965
+ * 全区画で同じ節(クラス refs-section・ul.refs)。並びは「根拠 → 関連の資料 → 置き換え」。
6966
+ * - 決定: 台帳の決めた場面・言い直しの項目(リンク+引用)→ 台帳に無い references はリンクだけの行を下に
6967
+ * - ルール: 「元の決定」(derived_from・複数なら全部・画面に無い決定は文字だけ)とその決定の項目(台帳に無ければ決定の references)
6968
+ * → 自分の項目と references。同じ根拠は 1 回だけ
6969
+ * - 用語・関係者: 項目を発言の日の新しい順に。最新 3 件を開き、古い分は「ほか N 件」で畳む → 台帳に無い references はリンクだけ
6970
+ * 台帳の項目はビルドが記録ごとのファイル(evidenceRef)に書き出してあり、詳細を開いたときに読む。読めなくても references のリンクだけで描く。
6971
+ * 何も無い記録では null(要素を作らない)。リンクは http(s) だけ(ビルドが解決済みでも画面で二重に見る)。
6972
+ * 🔴 引用は textContent だけで組む(innerHTML に入れない)
6973
+ */
6974
+ var EVIDENCE_QUOTE_MAX = 120;
6975
+ var EVIDENCE_OPEN_ROWS = 3;
6976
+ var evidenceCache = {};
6977
+ function evidenceHttp(u) { return typeof u === "string" && /^https?:\\/\\//i.test(u); }
6978
+ /** 引用 1 つ(「話し手:発言」)。120 字を超えたら頭の 120 字と「…続きを開く」、開くと全文(頭を二重にしない) */
6979
+ function evidenceQuote(q) {
6980
+ var box = el("div", { class: "evidence-quote" + (q.context ? " is-context" : "") });
6981
+ if (q.context) box.appendChild(el("span", { class: "evidence-context-tag", text: "前の発言" }));
6982
+ var who = q.speaker ? String(q.speaker) + ":" : "";
6983
+ var chars = Array.from(String(q.text || ""));
6984
+ var body = el("span", { class: "evidence-quote-text" });
6985
+ box.appendChild(body);
6986
+ if (chars.length <= EVIDENCE_QUOTE_MAX) {
6987
+ body.textContent = who + chars.join("");
6988
+ return box;
6989
+ }
6990
+ body.textContent = who + chars.slice(0, EVIDENCE_QUOTE_MAX).join("");
6991
+ var more = el("button", { type: "button", class: "evidence-more", text: "…続きを開く" });
6992
+ more.addEventListener("click", function () {
6993
+ body.textContent = who + chars.join("");
6994
+ if (more.parentNode) more.parentNode.removeChild(more);
6965
6995
  });
6996
+ box.appendChild(more);
6997
+ return box;
6998
+ }
6999
+ /** 台帳の項目 1 行(リンクか会議の 1 行・発言の日・引用) */
7000
+ function evidenceItemRow(it) {
7001
+ var li = el("li", { class: "evidence-row" });
7002
+ var head = el("div", { class: "evidence-head" });
7003
+ // 言い直し・書き写しの発言は、決めた場面の発言と見分けられるよう札を付ける
7004
+ if (it.kind === "restated") head.appendChild(el("span", { class: "evidence-origin-tag evidence-restated-tag", text: "言い直し" }));
7005
+ if (evidenceHttp(it.url)) head.appendChild(el("a", { href: it.url, target: "_blank", rel: "noopener", text: it.label || it.url }));
7006
+ else head.appendChild(el("span", { class: "evidence-text-ref", text: it.label || "" }));
7007
+ if (it.at) head.appendChild(el("span", { class: "evidence-at mono", text: it.at }));
7008
+ li.appendChild(head);
7009
+ var qs = Array.isArray(it.quotes) ? it.quotes.filter(function (q) { return q && typeof q.text === "string" && q.text; }) : [];
7010
+ if (qs.length) {
7011
+ var qbox = el("div", { class: "evidence-quotes" });
7012
+ // 前の発言を先に(小さく)、そのあと本人の発言
7013
+ qs.filter(function (q) { return q.context; }).concat(qs.filter(function (q) { return !q.context; })).forEach(function (q) { qbox.appendChild(evidenceQuote(q)); });
7014
+ li.appendChild(qbox);
7015
+ }
7016
+ return li;
7017
+ }
7018
+ /** リンクだけの行(台帳に無い references) */
7019
+ function evidenceLinkRow(ref) {
7020
+ var li = el("li", { class: "evidence-ref" });
7021
+ if (evidenceHttp(ref.url)) li.appendChild(el("a", { href: ref.url, target: "_blank", rel: "noopener", text: ref.label || ref.url }));
7022
+ else li.textContent = ref.label || "";
7023
+ return li;
7024
+ }
7025
+ /** 「元の決定」の行。画面にある決定なら題名で詳細へのリンク、無ければ ID の文字だけ */
7026
+ function evidenceOriginRow(id) {
7027
+ var li = el("li", { class: "evidence-origin" });
7028
+ li.appendChild(el("span", { class: "evidence-origin-tag", text: "元の決定" }));
7029
+ var d = byId[id];
7030
+ if (d && d._kind === "decision" && !d.deleted) {
7031
+ var a = el("a", { href: "?id=" + encodeURIComponent(id), text: humanTitle(d.title) });
7032
+ a.addEventListener("click", function (ev) {
7033
+ if (ev.defaultPrevented || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
7034
+ ev.preventDefault();
7035
+ go(id);
7036
+ });
7037
+ li.appendChild(a);
7038
+ } else {
7039
+ li.appendChild(el("span", { class: "mono", text: id }));
7040
+ }
7041
+ return li;
7042
+ }
7043
+ /** ルールの元の決定(derived_from の宛先・重ならない並び) */
7044
+ function evidenceOriginIds(r) {
7045
+ var out = [];
7046
+ (r.relations || []).forEach(function (x) {
7047
+ if (!x || x.rel !== "derived_from" || typeof x.target !== "string") return;
7048
+ var id = x.target.trim();
7049
+ if (id && out.indexOf(id) < 0) out.push(id);
7050
+ });
7051
+ return out;
7052
+ }
7053
+ /** 段の中身を組む。data は根拠のファイル(読めていなければ null=references のリンクだけ) */
7054
+ function fillEvidence(ul, r, sec, origins, data) {
7055
+ while (ul.firstChild) ul.removeChild(ul.firstChild);
7056
+ var seen = {};
7057
+ function fresh(k) {
7058
+ if (!k) return true;
7059
+ if (seen[k]) return false;
7060
+ seen[k] = true;
7061
+ return true;
7062
+ }
7063
+ function refRows(list, keys) {
7064
+ (list || []).forEach(function (ref, i) {
7065
+ if (!ref) return;
7066
+ var k = keys && typeof keys[i] === "string" ? keys[i] : (ref.url || ref.label || "");
7067
+ if (fresh(k)) ul.appendChild(evidenceLinkRow(ref));
7068
+ });
7069
+ }
7070
+ function itemRows(items, fold) {
7071
+ var rows = [];
7072
+ (Array.isArray(items) ? items : []).forEach(function (it) {
7073
+ if (it && fresh(it.key || it.url || it.label)) rows.push(evidenceItemRow(it));
7074
+ });
7075
+ if (!fold || rows.length <= EVIDENCE_OPEN_ROWS) { rows.forEach(function (li) { ul.appendChild(li); }); return; }
7076
+ rows.slice(0, EVIDENCE_OPEN_ROWS).forEach(function (li) { ul.appendChild(li); });
7077
+ var rest = rows.slice(EVIDENCE_OPEN_ROWS);
7078
+ var foldLi = el("li", { class: "evidence-fold" });
7079
+ var btn = el("button", { type: "button", class: "evidence-fold-btn", text: "ほか " + rest.length + " 件" });
7080
+ btn.addEventListener("click", function () {
7081
+ rest.forEach(function (li) { li.hidden = false; });
7082
+ if (foldLi.parentNode) foldLi.parentNode.removeChild(foldLi);
7083
+ });
7084
+ foldLi.appendChild(btn);
7085
+ ul.appendChild(foldLi);
7086
+ rest.forEach(function (li) { li.hidden = true; ul.appendChild(li); });
7087
+ }
7088
+ if (sec === "Rules") {
7089
+ var byOrigin = {};
7090
+ ((data && Array.isArray(data.origins)) ? data.origins : []).forEach(function (o) { if (o && typeof o.id === "string") byOrigin[o.id] = o; });
7091
+ origins.forEach(function (id) {
7092
+ ul.appendChild(evidenceOriginRow(id));
7093
+ var o = byOrigin[id];
7094
+ var d = byId[id];
7095
+ if (o && Array.isArray(o.items) && o.items.length) itemRows(o.items, false);
7096
+ else if (d && d._kind === "decision") refRows(d.references, o ? o.refKeys : null);
7097
+ });
7098
+ }
7099
+ itemRows(data ? data.items : null, sec === "Glossary" || sec === "Members");
7100
+ refRows(r.references, data ? data.refKeys : null);
7101
+ }
7102
+ /**
7103
+ * 根拠のファイルを読む。読めた・前に読んだ → その中身を同期で返す/読みに行った → undefined を返し、終わったら cb(失敗は null)/
7104
+ * 読みに行けない(fetch が無い)→ null。印(evidenceId)の食い違うファイルは使わない(別の記録の根拠を出さない)
7105
+ */
7106
+ function loadEvidence(r, cb) {
7107
+ var ref = r.evidenceRef;
7108
+ if (Object.prototype.hasOwnProperty.call(evidenceCache, ref)) return evidenceCache[ref];
7109
+ if (typeof fetch !== "function") return null;
7110
+ fetch(r.evidenceRef).then(function (res) {
7111
+ if (!res.ok) throw new Error("http " + res.status);
7112
+ return res.json();
7113
+ }).then(function (data) {
7114
+ var ok = data && data.v === 1 && data.id === r.evidenceId && Array.isArray(data.items) ? data : null;
7115
+ if (ok) evidenceCache[ref] = ok;
7116
+ cb(ok);
7117
+ }).catch(function () { cb(null); });
7118
+ return undefined;
7119
+ }
7120
+ function evidenceSection(r) {
7121
+ if (!r) return null;
7122
+ var sec = r._kind === "decision" ? "Decisions" : r._section;
7123
+ var origins = sec === "Rules" ? evidenceOriginIds(r) : [];
7124
+ var box = el("div", { class: "refs-section evidence-section" });
7125
+ box.appendChild(el("div", { class: "section-label", text: "根拠" }));
7126
+ var ul = el("ul", { class: "refs evidence-list" });
6966
7127
  box.appendChild(ul);
7128
+ var data = null;
7129
+ var pending = false;
7130
+ if (typeof r.evidenceRef === "string" && r.evidenceRef && r.evidenceCount > 0) {
7131
+ data = loadEvidence(r, function (got) {
7132
+ if (got) fillEvidence(ul, r, sec, origins, got);
7133
+ if (!ul.firstChild) { if (box.parentNode) box.parentNode.removeChild(box); }
7134
+ else box.hidden = false;
7135
+ });
7136
+ if (data === undefined) { pending = true; data = null; }
7137
+ }
7138
+ fillEvidence(ul, r, sec, origins, data);
7139
+ if (!ul.firstChild && !pending) return null;
7140
+ // 読み込み中で中身がまだ無い間は見出しだけを見せない(読めたら出す)
7141
+ if (!ul.firstChild) box.hidden = true;
6967
7142
  return box;
6968
7143
  }
6969
7144
  /** 候補の帯(症状)。行の札は「候補」とだけ言い、理由はこの帯 1 行に寄せる(繰り返さない) */
@@ -8181,7 +8356,7 @@ ${story_1.splitByNameLinks.toString()}
8181
8356
  text: "決定・ルール・用語・関係者と、その根拠(元のツール)のつながりの地図です。点をクリックすると、そのページや元のツール(Backlog・GitHub等)に移動できます。",
8182
8357
  },
8183
8358
  "explorer": {
8184
- text: "決定・ルール・用語・関係者を起点に、つながり(根拠・出典)を1列ずつ辿る表示です。行をクリックすると右に次の列が開きます。",
8359
+ text: "決定・ルール・用語・関係者を起点に、つながり(根拠)を1列ずつ辿る表示です。行をクリックすると右に次の列が開きます。",
8185
8360
  },
8186
8361
  "story": {
8187
8362
  text: "このプロジェクトの日々の動きを、日次・週次・月次に分けてまとめた記録です。直したいことや足したいことは、各カードの「GitHubで編集」か「AIで編集」から書けます。",
@@ -19202,6 +19377,8 @@ ${story_1.splitByNameLinks.toString()}
19202
19377
  // 「[object Object]」が画面に出る。語彙の外は「そのまま出す」が規則だが、**そのまま出せない値**
19203
19378
  // はこの 1 語に寄せる(色は中立のまま)。分類 B・決定ログ 20260920-686.md の B-19
19204
19379
  var LI_BASIS_UNREADABLE = "(根拠を読み取れません)";
19380
+ // basis が無い行(出どころを言わない)。空欄で出す(「本人が申告」に倒さない)
19381
+ var LI_BASIS_NONE = { text: "", tone: "is-plain" };
19205
19382
  // 🔴 **5 列目の「場」(scope)は #704 で足した。** engine は綴りだけ予約していて、いまは
19206
19383
  // 人が手で書いたときだけ入る。無い行は素の空(「いつから」と同じ——空が正常)
19207
19384
  var LI_COLS = ["道具", "この人のページ", "どうやって分かったか", "いつから", "場"];
@@ -19241,8 +19418,10 @@ ${story_1.splitByNameLinks.toString()}
19241
19418
  /**
19242
19419
  * 根拠 → 画面の言葉と色。
19243
19420
  *
19244
- * 🔴 **basis が無い行は「本人が申告」**(engine の決め。人がいちばん書きやすい
19245
- * id だけの 1 行が、いちばん強い宣言になる)。
19421
+ * 🔴 **basis が無い行は何も言わない(空)**(cortex-engine #1911 便 B4)。以前は「本人が申告」に倒していたが、
19422
+ * 記憶 v2 の書き手が書く機械の行も basis を持たないので、人が書いた行と機械の行が画面で区別できず、
19423
+ * 機械の結びを人の宣言のように見せていた。出どころはシルバー・ポータルの確定・宣言の置き場にあり、frontmatter には持たない。
19424
+ * 明記された basis(declared など)は今までどおりの言葉で出す。
19246
19425
  * 🔴 **語彙の外はそのまま出して、中立の色に置く**——知らない根拠を、確かな側にも
19247
19426
  * 機械の側にも倒さない
19248
19427
  */
@@ -19251,7 +19430,7 @@ ${story_1.splitByNameLinks.toString()}
19251
19430
  // 以前は typeof raw === "string" でない値を一律に空扱いしていたので、
19252
19431
  // basis: 123 や basis: {}(engine が形を崩した日・人が手で書き損ねた日)が
19253
19432
  // **人の宣言**として確かな側の色で出ていた。読めない値を昇格させない
19254
- if (raw == null) return LI_BASIS.declared;
19433
+ if (raw == null) return LI_BASIS_NONE;
19255
19434
  if (typeof raw !== "string") {
19256
19435
  var shown = typeof raw === "number" || typeof raw === "boolean" || typeof raw === "bigint"
19257
19436
  ? String(raw)
@@ -19259,7 +19438,7 @@ ${story_1.splitByNameLinks.toString()}
19259
19438
  return { text: shown, tone: "is-plain" };
19260
19439
  }
19261
19440
  var key = raw.trim();
19262
- if (key === "") return LI_BASIS.declared;
19441
+ if (key === "") return LI_BASIS_NONE;
19263
19442
  var lower = key.toLowerCase();
19264
19443
  if (Object.prototype.hasOwnProperty.call(LI_BASIS, lower)) return LI_BASIS[lower];
19265
19444
  return { text: key, tone: "is-plain" };
@@ -19272,6 +19451,7 @@ ${story_1.splitByNameLinks.toString()}
19272
19451
  * Slack のユーザーのページを組むワークスペースのドメイン(決められなければ空文字)。
19273
19452
  *
19274
19453
  * 🔴 **既定の決め方(分類 B・決定ログ 20260923-704.md)**:
19454
+ * 0. 行の場がホストの形(<名> / <組織>.enterprise)なら、それをそのまま使う(engine が登録チャンネルの URL から書く・#1911)
19275
19455
  * 1. 行の場(scope)が接続済みワークスペースの team_id(T…)と一致すれば、その
19276
19456
  * ワークスペース。一致した先にドメインが無ければ**リンク無し**(他へ倒さない)。
19277
19457
  * 場が team_id の形なのにどれとも一致しなければ**リンク無し**
@@ -19285,6 +19465,10 @@ ${story_1.splitByNameLinks.toString()}
19285
19465
  var list = slackWorkspaces() || [];
19286
19466
  var pick = null;
19287
19467
  var want = typeof scope === "string" ? scope.trim() : "";
19468
+ // 🔴 **場がワークスペースのホストそのもの(<名> か <組織>.enterprise)なら、それをそのまま使う**(cortex-engine #1911)。
19469
+ // engine が、人が操作画面で貼ったチャンネルの URL のホストを scope に書く。台帳と食い違っても、人が貼った URL を根拠にする。
19470
+ // 形は許可リスト(LI_SLACK_DOMAIN / LI_SLACK_ENT_DOMAIN)で検査する。team_id(T… / E…・大文字)は下の道へ
19471
+ if (want !== "" && !LI_SLACK_TEAM.test(want) && (LI_SLACK_DOMAIN.test(want) || LI_SLACK_ENT_DOMAIN.test(want))) return want.toLowerCase();
19288
19472
  if (want !== "") {
19289
19473
  for (var i = 0; i < list.length; i++) {
19290
19474
  if (list[i] && list[i].teamId === want) { pick = list[i]; break; }
@@ -19940,8 +20124,8 @@ ${story_1.splitByNameLinks.toString()}
19940
20124
  app.appendChild(genBody);
19941
20125
  loadBody(r, genBody);
19942
20126
 
19943
- // 参照(rule.card)。ルールだけ。決定の詳細と同じ「参照」の節(ビルドが解決した出典リンク)
19944
- var refs2 = referencesSection(r);
20127
+ // 根拠(便 B3)。ルール・用語・関係者も決定の詳細と同じ「根拠」の節
20128
+ var refs2 = evidenceSection(r);
19945
20129
  if (refs2) app.appendChild(refs2);
19946
20130
 
19947
20131
  // 関連の資料(#1094 F8)。本文の後。ルール・用語にしか載らない
@@ -19976,11 +20160,11 @@ ${story_1.splitByNameLinks.toString()}
19976
20160
  * kind: "gold"=Goldのレコードどうしを結ぶ線/"evidence"=Goldがその根拠を指す線(弱いつながり)。 */
19977
20161
  var REL_META = {
19978
20162
  based_on: { color: "#2563eb", label: "根拠" },
19979
- derived_from: { color: "#0d9488", label: "生成元" },
20163
+ derived_from: { color: "#0d9488", label: "元の決定" },
19980
20164
  relates_to: { color: "#94a3b8", label: "関連" },
19981
20165
  supersedes: { color: "#dc2626", label: "置換" },
19982
- reference: { color: "#d1d5db", label: "参照" },
19983
- source: { color: "#d1d5db", label: "出典" },
20166
+ reference: { color: "#d1d5db", label: "根拠" },
20167
+ source: { color: "#d1d5db", label: "根拠" },
19984
20168
  decided_by: { color: "#f472b6", label: "決めた人" },
19985
20169
  };
19986
20170
  var REL_COLOR = {};
@@ -20846,14 +21030,14 @@ ${story_1.splitByNameLinks.toString()}
20846
21030
  app.appendChild(decBody);
20847
21031
  loadBody(d, decBody);
20848
21032
 
20849
- var refsBox = referencesSection(d);
21033
+ var refsBox = evidenceSection(d);
20850
21034
  if (refsBox) app.appendChild(refsBox);
20851
21035
 
20852
- // 関連の資料(#1094 F8)。「参照」(根拠)の後・置き換えの前。根拠と混ぜない
21036
+ // 関連の資料(#1094 F8)。「根拠」の後・置き換えの前。根拠と混ぜない
20853
21037
  var relm = relatedMaterialsSection(d);
20854
21038
  if (relm) app.appendChild(relm);
20855
21039
 
20856
- // 置き換え(#699 F3)。本文・参照の後、「その後の記録」の前。置き換えの無い決定では何も作らない
21040
+ // 置き換え(#699 F3)。本文・根拠の後、「その後の記録」の前。置き換えの無い決定では何も作らない
20857
21041
  var sup = supersedeSection(d);
20858
21042
  if (sup) app.appendChild(sup);
20859
21043
 
@@ -20875,8 +21059,8 @@ ${story_1.splitByNameLinks.toString()}
20875
21059
  // superseded_by は**辺としては描かない**(ビルド側の MIRROR_RELS)。ここに置くのは
20876
21060
  // レコード情報(frontmatter)のパネルが英語の語のまま出さないためだけ(凡例の REL_LABEL には足さない)
20877
21061
  var REL_LABEL = {
20878
- based_on: "根拠", derived_from: "由来", relates_to: "関連",
20879
- supersedes: "置換", superseded_by: "置換先", reference: "参照", source: "出典", link: "リンク",
21062
+ based_on: "根拠", derived_from: "元の決定", relates_to: "関連",
21063
+ supersedes: "置換", superseded_by: "置換先", reference: "根拠", source: "根拠", link: "リンク",
20880
21064
  about: "話題",
20881
21065
  decided_by: "決めた人",
20882
21066
  };
@@ -21234,7 +21418,8 @@ ${story_1.splitByNameLinks.toString()}
21234
21418
  v.forEach(function (ent) {
21235
21419
  var line = el("div", { class: "fm-li" });
21236
21420
  if (ent && typeof ent === "object" && !Array.isArray(ent) && typeof ent.id === "string") {
21237
- line.textContent = ent.id + "(" + liBasis(ent.basis).text + ")";
21421
+ var basisText = liBasis(ent.basis).text;
21422
+ line.textContent = basisText ? ent.id + "(" + basisText + ")" : ent.id;
21238
21423
  } else {
21239
21424
  // 形が外れている要素も消さずに読める形で残す(文字列で書かれた欄を含む)
21240
21425
  line.textContent = typeof ent === "string" ? ent : JSON.stringify(ent) || String(ent);
package/dist/story.js CHANGED
@@ -656,7 +656,9 @@ function storyDigestParts(content) {
656
656
  const at = body.findIndex((l, i) => i > end && l.trim() === HUMAN_HEADING);
657
657
  // 機械の部分の `#` の見出しの行は落とす(流れは段落だけの約束。見出しが紛れると後ろの文が別の節に見える)
658
658
  const main = withoutHiddenBoldGroups((at < 0 ? body : body.slice(0, at)).map((l) => unlinkRelativeMd(l)).filter((l) => !STORY_MARKER_RE.test(l.trim()) && !/^\s{0,3}#{1,6}(\s|$)/.test(l)))
659
- .filter((l) => !MACHINE_WORD_RE.test(l));
659
+ .filter((l) => !MACHINE_WORD_RE.test(l))
660
+ // 前の書き手が残した固定の見出し「ほかに動いた筋:」は、画面では「論点」と呼ぶ(記録は書き換えない・common.strand-word)
661
+ .map((l) => l.replace(/^ほかに動いた筋(?=\s*[::])/, "ほかに動いた論点"));
660
662
  const human = (at < 0 ? [] : body.slice(at + 1)).filter((l) => !STORY_MARKER_RE.test(l.trim()));
661
663
  let phase = null;
662
664
  const flow = [];
package/dist/view-dir.js CHANGED
@@ -32,6 +32,7 @@ exports.viewCountsLine = viewCountsLine;
32
32
  * ## 名前(engine の宣言表 `publish: "viewer"` の行・V10a の `VIEW_HELD`)
33
33
  *
34
34
  * - 読む 10 本のうち、記憶側が置くのは 8 本+順位 1 本(`VIEW_NAMES`)
35
+ * - 根拠の台帳(`memory-evidence`・便 B3)は案件リポに置き公開されない(engine の宣言表 `viewer-direct`)。目次の名前に入れず、読み手がいつも `<repo>/.cortex` から読む
35
36
  * - 確認の帳簿(`gold-confirmation`・再導出の帯の読み手 `readRederivationLedgers` が読む)は置かれないので、いつも `.cortex` から読む
36
37
  * - 名寄せの提案(`gold-proposals`)はビューアが読まなくなった(cortex-engine #1094 B-viewer V4・問いはポータルから)。置かれても使わない
37
38
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.167",
3
+ "version": "0.12.172",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,