@takagaki/cortex-decisions-viewer 0.12.85 → 0.12.87

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.
@@ -24,7 +24,7 @@ exports.humanFollowupOf = humanFollowupOf;
24
24
  *
25
25
  * | 帳簿 | 何を持つか | 書き手 |
26
26
  * | --- | --- | --- |
27
- * | `.cortex/gold-confirmation.json` | レコードごとの「人が確認した版」と、そこから変わったか(**sha だけ**) | 夜間(全置換・git から作り直せる) |
27
+ * | `.cortex/gold-confirmation.json` | レコードごとの「人が確認した版」と、その後の機械の書き直しの最新(`rewritten`・中身の指紋つき・#799) | 夜間(全置換・git から作り直せる) |
28
28
  * | `.cortex/gold-rederivation.json` | 書き直しの根拠の件数・shadow の「書くつもりだった差分」 | 夜間(追記・区画ごとの節) |
29
29
  * | `.cortex/gold-activity.json` | 「最後に見た日」と触れた日の列(+観測の到達点 `cursor`) | 夜間(追記・剪定つき) |
30
30
  *
@@ -46,7 +46,6 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
46
46
  const promises_1 = __importDefault(require("node:fs/promises"));
47
47
  const node_path_1 = __importDefault(require("node:path"));
48
48
  const gray_matter_1 = __importDefault(require("gray-matter"));
49
- const commit_classify_1 = require("./commit-classify");
50
49
  /** 帳簿の版(違ったら空として扱う・engine `LEDGER_VERSION`) */
51
50
  exports.LEDGER_VERSION = 1;
52
51
  /** 帯を出す区画。**用語とメンバーだけ**(Decisions は積み上げ・Rules は便 13) */
@@ -147,6 +146,16 @@ async function readRederivationLedgers(repoRoot) {
147
146
  : [];
148
147
  // 中身の指紋は**文字列のときだけ**持つ(engine #546 が書く。古い帳簿の行には無い)
149
148
  const contentSha = typeof confirmed.content_sha === "string" ? confirmed.content_sha : "";
149
+ // 機械の書き直し(#799)。**オブジェクトのときだけ**持つ。欄が無い(#799 より前の帳簿)・
150
+ // null(確認の後に機械の書き直しが無い)はどちらもキーごと無い=帯は出ない
151
+ const rw = asRecord(row?.rewritten);
152
+ const rewritten = rw
153
+ ? {
154
+ at: typeof rw.at === "string" ? rw.at : "",
155
+ commit: typeof rw.commit === "string" ? rw.commit : "",
156
+ contentSha: typeof rw.content_sha === "string" ? rw.content_sha : "",
157
+ }
158
+ : null;
150
159
  out.confirmation.set(p, {
151
160
  at: confirmed.at != null ? String(confirmed.at) : "",
152
161
  by: confirmed.by != null ? String(confirmed.by) : "",
@@ -154,6 +163,13 @@ async function readRederivationLedgers(repoRoot) {
154
163
  commit: commit,
155
164
  changedBy: changedBy,
156
165
  ...(contentSha ? { contentSha: contentSha } : {}),
166
+ ...(rewritten ? { rewritten: rewritten } : {}),
167
+ // 🔴 **厳密に true のときだけ**("true" や 1 を削除と読まない)
168
+ ...(confirmed.deleted === true ? { deleted: true } : {}),
169
+ // 基準コミットでの path(改名の前の確認)。**空でない文字列で、今の path と違うときだけ**持つ
170
+ ...(typeof confirmed.path === "string" && confirmed.path !== "" && confirmed.path !== p
171
+ ? { basePath: confirmed.path }
172
+ : {}),
157
173
  });
158
174
  }
159
175
  }
@@ -350,9 +366,10 @@ function sameRederivedContent(a, b) {
350
366
  /**
351
367
  * 「説明文と本文」の指紋。**帳簿の `confirmed.content_sha` と突き合わせる綴り**。
352
368
  *
353
- * 🔴 比べる相手は engine #546 が書く `confirmed.content_sha`。**一致したときだけ
354
- * 「変わっていない」と言い、食い違ったら git へ落とす**(`attachRederived`)——engine が
355
- * 別の綴りで指紋を作っていた場合に、全レコードへ誤報が出るのを避けるため。
369
+ * 🔴 比べる相手は engine が書く 2 つの指紋——`rewritten.content_sha`(機械が書き直した版・#799)と
370
+ * `confirmed.content_sha`(人が確認した版・#546)。帯は「今の指紋 = 書き直した版 ≠ 確認した版」の
371
+ * ときだけ出る(`rederivedBandOf`)。**綴りがずれると「今 = 書き直した版」が当たらなくなり、
372
+ * 帯は出ない側に倒れる**(誤報より無言)。
356
373
  *
357
374
  * engine の写し: `cortex-engine/scripts/rederive/content-sha.mjs`・一致の番人:
358
375
  * `cortex-engine/test/rederive-history.test.mjs` の #546 の節(ここを変えたら engine の `VIEWER_SHA` を計算し直す)
@@ -362,12 +379,12 @@ function contentShaOf(raw) {
362
379
  return node_crypto_1.default.createHash("sha256").update(c.description + "\u0000" + c.body, "utf8").digest("hex");
363
380
  }
364
381
  /**
365
- * そのレコードが印の**候補**なら、人が確認した版を git から引くための key
366
- * (`<基準コミット>:<path>`)。候補でなければ null。
382
+ * そのレコードが帯の**候補**か(決定表の行 0〜4・中身を読む前の安い行だけ)。候補なら
383
+ * `<基準コミット>:<path>`(人が確認した版を git から引くときの key)、候補でなければ null。
367
384
  *
368
- * 🔴 **決定表の行 0〜3 と同じ門**(`rederivedBandOf` の手前の安い行だけ)。ここで候補を
369
- * 絞ってから `readBlobsAt` に**まとめて 1 回**渡す——全レコードぶんの key を作ると、
370
- * 用語が 1400 件ある案件で毎晩 1400 個の blob を読むことになる。
385
+ * 🔴 **ここで候補を絞ってから今の版を読む**——全レコードぶん作業ツリーを読むと、
386
+ * 用語が 1400 件ある案件で毎晩 1400 件を読むことになる。git に渡るのはさらにその中の
387
+ * 「帳簿に人が確認した版の指紋が無い」記録だけ(`attachRederived`)。
371
388
  */
372
389
  function rederivedContentKey(recordPath, status, ledgers) {
373
390
  if (!isConfirmedStatus(status))
@@ -375,132 +392,83 @@ function rederivedContentKey(recordPath, status, ledgers) {
375
392
  if (!BAND_SECTION_RE.test(recordPath))
376
393
  return null;
377
394
  const conf = ledgers.confirmation.get(recordPath);
378
- if (!conf || !conf.commit || !conf.changedBy.length)
395
+ if (!conf || !conf.commit || conf.deleted || conf.basePath)
396
+ return null;
397
+ const rw = conf.rewritten;
398
+ if (!rw || !rw.contentSha || !rw.at)
379
399
  return null;
380
400
  return `${conf.commit}:${recordPath}`;
381
401
  }
382
402
  /**
383
- * そのレコードに帯を立てるか(#665 の**決定表**・上から順に評価し、当たった行で終わる)。
403
+ * そのレコードに帯を立てるか(#799 の**決定表**・上から順に評価し、当たった行で終わる)。
384
404
  *
385
405
  * | 順 | 条件 | 結果 |
386
406
  * | --- | --- | --- |
387
- * | **0** | **その記録の `status` が `draft`(=「AI生成・未確認」の札が出る)** | **立てない**(#667) |
388
- * | 1 | path が用語・メンバーのレコードでない | 立てない |
389
- * | 2 | `confirmation` に行が無い/基準コミットが空/確認日が日時として読めない | 立てない |
390
- * | 3 | `changed_by` が空 | 立てない |
391
- * | 4 | `changed_by` の 1 件ずつに 4a〜4e を当てて**残りが 0 件** | 立てない |
392
- * | 4a | sha が基準コミットと同じ | その sha を落とす |
393
- * | 4b | sha が git の窓に無い | 落とす(**不明**・`miss.unresolved` に数える) |
394
- * | 4c | 分類が「人」または「不明」 | 落とす |
395
- * | 4d | **どちらかの綴りで引っ越し** | 落とす |
396
- * | 4e | author 日付が確認日**以前**(epoch 比較) | 落とす |
397
- * | 5 | 残りのうち最も新しい author 日付を「書き直した日」にする | 候補になる |
398
- * | **8** | **人が確認した版から「説明文(`description`)か本文」が変わったか**(`verdict`) | **`changed` のときだけ立てる** |
399
- * | 6 | 根拠は、帳簿の行の `commit` が `changed_by` に入っているときだけ出す | 出す/出さない |
400
- * | 7 | 基準コミットが削除のコミットなら「戻す」を出さない(`baseDeleted`) | — |
401
- *
402
- * 🔴 **行 8 がこの機能の本丸**(2026-09-20 の誤報)。前はここが無く、「確認の基準コミットより
403
- * 後に機械のコミットがその path に 1 件でもあるか」だけで立てていた——**ファイルの中身を
404
- * 1 バイトも見ていなかった**ので、移行でフロントマターの使わない 1 行(`side: cm`)が
405
- * 消えただけの回にも、関係者 11 人の画面に「変わりました」が出た。
407
+ * | 0 | その記録の `status` が `draft`(=「AI生成・未確認」の札が出る) | 出さない(#667) |
408
+ * | 1 | path が用語・メンバーのレコードでない | 出さない |
409
+ * | 2 | `confirmation` に行が無い/基準コミットが空 | 出さない |
410
+ * | 3 | **基準コミットがこの path を消した(`confirmed.deleted`)/基準コミットでの path が今と違う(`confirmed.path`・改名の前の確認)** | **出さない** |
411
+ * | 4 | **`rewritten` が無い(古い帳簿)・null・指紋が空・日時が空** | **出さない** |
412
+ * | 5 | 今の版が読めない | 出さない(`miss.unreadable` に数える) |
413
+ * | 6 | **今の版の指紋 ≠ `rewritten.content_sha`**(後から人や機械が直した) | **出さない** |
414
+ * | 7a | 帳簿に人が確認した版の指紋がある: **今の指紋 = その指紋**(元に戻った) | **出さない** |
415
+ * | 7b | 指紋が無い(engine が自信を持てない形・#546 より前): git の基準版と比べて `changed` でない | 出さない |
416
+ * | 8 | 根拠は、根拠の帳簿の行の `commit` が `rewritten.commit` と同じときだけ出す | 出す/出さない |
417
+ *
418
+ * 🔴 **人か機械かを画面が見分けない**(#799)。前は `changed_by` の sha ごとに git の素性
419
+ *(名義・件名・trailer)を引き、engine の分類の**写し**で機械の書き直しを拾っていた。
420
+ * 写しはずれる(App 名義の画面操作を機械と読む日が来る)ので、見分けは engine に 1 か所で
421
+ * 寄せ、画面は「今の版が、engine が機械の書き直しと呼んだ版そのものか」だけを指紋で見る。
422
+ * 後から人や別の機械が直していれば指紋が合わないので、「戻す」が人の編集を消すことはない。
406
423
  *
407
424
  * @param status その記録の `rstatus`(`String()` 済み)。**2 番目の必須引数**にしてある——
408
425
  * 末尾の任意引数にすると、渡し忘れが「行 0 の門が静かに開く」になる(設計レビュー 🔴3)
409
- * @param verdict 行 8 の答えを引く関数(`(recordPath, baseCommit) => ContentVerdict`)。
410
- * 🔴 **必須引数**にしてある——任意にすると、渡し忘れが「中身を見ない昔の判定」に静かに戻る。
411
- * `changed` 以外(`same` / `unknown`)は**立てない**
426
+ * @param nowShaOf 今の版(作業ツリー)の `contentShaOf`。読めなければ null。
427
+ * 🔴 **必須引数**——任意にすると、渡し忘れが「指紋を見ずに出す」に静かに倒れる
428
+ * @param verdict 行 7b の答えを引く関数(`(recordPath, baseCommit) => ContentVerdict`)。
429
+ * 帳簿に人が確認した版の指紋がある記録では**呼ばない**
412
430
  * @param miss 立たなかった理由を数える器(呼び出し側で 1 つ作って全レコードで共有する)
413
431
  */
414
- function rederivedBandOf(recordPath, status, ledgers, identities, verdict, miss) {
415
- // 0. 「AI生成・未確認」の札が出る記録には帯を重ねない(#667)。**綴りは 1 か所**
416
- //(ここで `status === "draft"` と直書きすると、判定が 2 つに増えて一致が崩れる)
417
- if (!isConfirmedStatus(status))
418
- return null;
419
- if (!BAND_SECTION_RE.test(recordPath))
432
+ function rederivedBandOf(recordPath, status, ledgers, nowShaOf, verdict, miss) {
433
+ // 0〜4. 中身を読む前の門(候補の絞り込みと同じ 1 つの関数に寄せる。2 つに増やすと一致が崩れる)
434
+ if (!rederivedContentKey(recordPath, status, ledgers))
420
435
  return null;
421
436
  const conf = ledgers.confirmation.get(recordPath);
422
- // 基準コミットが空の行はここで落とす(読み手は並びのために持っている)
423
- if (!conf || !conf.commit || !conf.changedBy.length)
437
+ const rw = conf.rewritten;
438
+ // 5. 今の版の指紋
439
+ const now = nowShaOf(recordPath);
440
+ if (now == null) {
441
+ if (miss)
442
+ miss.unreadable.add(recordPath);
424
443
  return null;
425
- // 確認日が読めない帳簿では「後か前か」を言えないので立てない(順序の確かめが要)
426
- const confirmedMs = Date.parse(conf.at);
427
- if (!Number.isFinite(confirmedMs))
428
- return null;
429
- let newest = null;
430
- /** 確認より後の**人**のコミットの最新(帳簿が前の夜のもので、基準点がまだ動いていない夜) */
431
- let newestHuman = 0;
432
- for (const sha of conf.changedBy) {
433
- // 4a. 基準コミット自身が並んでいる帳簿(作り直し前・手で書いたもの)に引っかからない
434
- if (sha === conf.commit)
435
- continue;
436
- const id = identities.get(sha);
437
- if (!id) {
438
- // 4b. 窓の外・浅い clone = 見分けられない(不明)。**画面には出さずログに数える**
439
- //(設計レビュー 🟡10: 帯が静かに消える経路がこれ)
440
- if (miss)
441
- miss.unresolved.add(sha);
442
- continue;
443
- }
444
- // 4e. 🔴 **確認より後であることを git の日付(epoch)で確かめる。** `changed_by` は engine が
445
- // 「基準点より後」で作るが、帳簿は前の夜のものが残りうる(全置換に失敗した回・人が後から
446
- // 確認して基準点が動いた回)。ここを外すと**確認より前の変更で帯が立つ**。
447
- // ⚠️ 「後か」の真の正本は engine が見た git のトポロジ順(`history.mjs:141-145`)で、
448
- // ここは**安全網**。author 日付は rebase・cherry-pick で前後しうるので、
449
- // ずれたときは「立てない」側に倒れる(設計レビュー 🟡12)
450
- const ms = Date.parse(id.at);
451
- if (!Number.isFinite(ms) || ms <= confirmedMs)
452
- continue;
453
- // 4c・4d. 機械の書き直しだけを残す(件名は分類と同じものを渡す)
454
- const classified = (0, commit_classify_1.classifyGoldCommit)({
455
- authorName: id.authorName,
456
- authorEmail: id.authorEmail,
457
- subject: id.subject,
458
- body: id.body,
459
- });
460
- if (!(0, commit_classify_1.isMachineRewrite)(classified, id.subject)) {
461
- if (classified.by === "human" && ms > newestHuman)
462
- newestHuman = ms;
463
- continue;
464
- }
465
- if (!newest || ms > newest.ms)
466
- newest = { at: id.at, ms: ms };
467
444
  }
468
- if (!newest)
469
- return null;
470
- // 🔴 **人が最後に言ったなら立てない。** engine は基準点を「最後に人が触ったコミット」に
471
- // 置くので、正しい帳簿の `changed_by` に人のコミットは入らない(`history.mjs:141`)。
472
- // だが前の夜の帳簿が残った朝には「機械の書き直し → その後の人の編集」が並びうる。
473
- // そこで帯を立てると、**「戻す」がその人の新しい編集ごと基準コミットへ戻す**——この機能で
474
- // いちばん重い壊れ方なので、既定(立てない側)に倒す。engine が次の夜に基準点を動かす
475
- if (newestHuman > newest.ms)
445
+ // 6. 🔴 **今の版が、engine が「機械の書き直し」と呼んだ版そのものか。** 違えば、その後に
446
+ // 人か別の機械が直している——そこで帯を出すと「戻す」がその直しごと消す
447
+ if (now !== rw.contentSha)
476
448
  return null;
477
- // 8. 🔴 **中身が変わったか。** ここまでは「機械がこの path を触ったか」しか見ていない。
478
- // 触っただけ(フロントマターの使わない欄が移行で消えた・`identities` の欄が足された)で
479
- // 印を立てると、説明文が 1 文字も変わっていない回に「変わりました」が出る(2026-09-20 の誤報)。
480
- // 引けないとき(`unknown`)も立てない——誤報より無言
481
- if (verdict(recordPath, conf.commit) !== "changed")
449
+ // 7. 人が確認した版から中身が変わっているか
450
+ if (conf.contentSha) {
451
+ // 7a. 機械が書き直したが、中身は人が確認した版と同じ(書き戻った・欄だけ触った)
452
+ if (now === conf.contentSha)
453
+ return null;
454
+ }
455
+ else if (verdict(recordPath, conf.commit) !== "changed") {
456
+ // 7b. 指紋が無い行は git の基準版と比べる(`same` も `unknown` も出さない)
482
457
  return null;
483
- // 6. 根拠の件数は「この帯のコミットの記録」から出す(別の夜の数字を並べない)
458
+ }
459
+ // 8. 根拠の件数は「この書き直しのコミットの記録」から出す(別の夜の数字を並べない)
484
460
  const red = ledgers.rederivation.get(recordPath);
485
- const evidence = red && red.evidence != null && red.commit && conf.changedBy.includes(red.commit)
486
- ? red.evidence
487
- : null;
488
- // 7. 基準コミットが削除のコミットか(窓の外なら分からないので false 側=リンクは出す。
489
- // プロンプト側に「取れなかったときの逃げ道」を書いてある)
490
- const base = identities.get(conf.commit);
491
- const baseDeleted = base ? commit_classify_1.DELETE_SUBJECT_RE.test(base.subject) : false;
461
+ const evidence = red && red.evidence != null && red.commit && rw.commit && red.commit === rw.commit ? red.evidence : null;
492
462
  return {
493
463
  confirmedAt: conf.at,
494
464
  // 🔴 **券の無い確認では名前を器に入れない**(設計レビュー 🟡4)。画面が出さないだけだと、
495
- // 名前は `site-data` の JSON として全案件のページに配られ続ける(`commit-classify.ts`
496
- // の `Operated-By`「値そのものを持ち回さない」と同じ流儀)。しかも engine の `by` は
465
+ // 名前は `site-data` の JSON として全案件のページに配られ続ける。しかも engine の `by` は
497
466
  // `confirmer || authorName` の合成で、**券が無ければ「ただ触った人」でしかない**
498
467
  confirmedBy: conf.verified ? conf.by : "",
499
468
  confirmedVerified: conf.verified,
500
469
  baseCommit: conf.commit,
501
- changedAt: newest.at,
470
+ changedAt: rw.at,
502
471
  ...(evidence != null ? { evidence: evidence } : {}),
503
- ...(baseDeleted ? { baseDeleted: true } : {}),
504
472
  };
505
473
  }
506
474
  /**
package/dist/render.js CHANGED
@@ -4237,12 +4237,14 @@ function buildTutorialData(site, repoSlug) {
4237
4237
  prompt: TUT_BACKLOG_PROMPT,
4238
4238
  });
4239
4239
  }
4240
- // 画面インベントリを読む体験(デザインを同期している案件のみ出す)
4240
+ // デザインの指摘を読む体験(デザインを同期している案件のみ出す)。
4241
+ // #727 K10 で取り込むのは Figma のコメント(デザイン/指摘/)だけになり、画面の本体は
4242
+ // AI が Figma の MCP で開く。移行期は inventory が残る案件もあるので、文言はどちらでも嘘にしない
4241
4243
  const usesDesign = capOn("デザイン") ?? internals.some((s) => s.kind === "デザイン" && s.enabled !== false);
4242
4244
  if (usesDesign) {
4243
4245
  specs.push({
4244
4246
  title: "デザインを読んでみよう",
4245
- desc: "画面インベントリから1画面を選んで理解する",
4247
+ desc: "Figmaの指摘から1画面を選んで理解する",
4246
4248
  prompt: TUT_DESIGN_PROMPT,
4247
4249
  });
4248
4250
  }
@@ -4310,10 +4312,11 @@ const TUT_BACKLOG_PROMPT = [
4310
4312
  "今回はコメントの追加だけです。課題の削除・クローズ・属性変更はしないでください。",
4311
4313
  ].join("\n");
4312
4314
  const TUT_DESIGN_PROMPT = [
4313
- "デザイン/inventory/ の画面インベントリから画面を1つ選び、それがどんな画面なのかを説明してください。1画面1ファイルで、frontmatter に安定ID(design:{fileKey}:{nodeId})とFigmaへのディープリンクがあります。",
4314
- "どの画面が見たいかは、まずわたしに質問してください(希望が無ければ主要そうな画面をあなたが選んでかまいません)。",
4315
- "画面の役割・主な要素・前後の画面とのつながりを説明し、関連する決定・用語・課題があれば安定IDをたどって補足し、最後にFigmaで実物を開くリンクを提示してください。",
4316
- "読むだけの練習です。インベントリはFigmaからの同期で上書きされる生成物なので、手で編集しないでください。",
4315
+ "デザイン/指摘/ に取り込んだ Figma のコメント(Figmaファイルごとのディレクトリに月ごとの md)から指摘を1つ選び、それがどの画面への指摘なのかを説明してください。各指摘には安定ID(figma-comment:{fileKey}:{commentId})と、その画面をFigmaで開くリンクがあります。",
4316
+ "画面そのものは取り込んでいません。Figma の MCP で開いて読んでください(プロジェクトによっては デザイン/inventory/ が残っているので、あれば手がかりに使ってかまいません)。Figma の MCP が使えないときは、そう伝えてリンクを示すところで止めてください。",
4317
+ "どの指摘・画面が見たいかは、まずわたしに質問してください(希望が無ければ未解決の指摘をあなたが選んでかまいません)。",
4318
+ "画面の役割・主な要素と、指摘が何を求めているかを説明し、関連する決定・用語・課題があれば安定IDをたどって補足し、最後にFigmaで実物を開くリンクを提示してください。",
4319
+ "読むだけの練習です。デザイン/ の下はFigmaからの同期で上書きされる生成物なので手で編集せず、Figma にコメントも書き込まないでください。",
4317
4320
  ].join("\n");
4318
4321
  const TUT_SLACK_PROMPT = [
4319
4322
  "Cortex のチュートリアルを完了したことを、このプロジェクトの Slack チャンネルに投稿してください。最後のステップです。",
@@ -4983,6 +4986,13 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
4983
4986
  .badge.followup-open { background: #eff6ff; color: #1e40af; border-color: #bfdbfe; }
4984
4987
  .badge.followup-superseded, .badge.followup-dropped { background: #f5f5f4; color: #57534e; border-color: #d6d3d1; }
4985
4988
  .followup-section { margin: 14px 0 4px; }
4989
+ /* 置き換え(#699 F3)。印は「その後」の置き換え済みと同じ色(同じ意味なので) */
4990
+ .badge.retired { background: #f5f5f4; color: #57534e; border: 1px solid #d6d3d1; }
4991
+ .supersede-section { margin: 14px 0 4px; }
4992
+ .supersede-list { list-style: none; margin: 0 0 8px; padding: 0; }
4993
+ .supersede-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; font-size: 13.5px; line-height: 1.7; padding: 2px 0; }
4994
+ .supersede-id { font-size: 12.5px; color: var(--muted); word-break: break-all; }
4995
+ .supersede-unconfirmed { font-size: 12px; color: #92400e; background: #fffbeb; border: 1px solid #fde68a; border-radius: 4px; padding: 0 6px; }
4986
4996
  .followup-human { font-size: 13.5px; margin: 0 0 6px; color: #166534; }
4987
4997
  .followup-list, .followup-evidence { list-style: none; margin: 0 0 8px; padding: 0; }
4988
4998
  .followup-row, .followup-evidence-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; font-size: 13.5px; line-height: 1.7; padding: 2px 0; }
@@ -6556,6 +6566,7 @@ ${calendarAddUrl.toString()}
6556
6566
  var FOLLOWUP_FILTER_LABEL = { "followup-done": "その後: 完了", "followup-open": "その後: 進行中", "followup-none": "その後: 記録なし" };
6557
6567
  function statusLabel(v) {
6558
6568
  if (Object.prototype.hasOwnProperty.call(FOLLOWUP_FILTER_LABEL, v)) return FOLLOWUP_FILTER_LABEL[v];
6569
+ if (v === RETIRED_FILTER) return "置き換え済み";
6559
6570
  return v === "draft" ? "未確認のみ" : v === "proposed" ? "提案ありのみ" : v;
6560
6571
  }
6561
6572
  /**
@@ -6568,6 +6579,8 @@ ${calendarAddUrl.toString()}
6568
6579
  var opts = [];
6569
6580
  if (hasUnverified(items)) opts.push("draft");
6570
6581
  if ((items || []).some(hasProposals)) opts.push("proposed");
6582
+ // 置き換え済み(#699 F3)。決定にしか付かない印なので、決定の一覧でだけ選択肢になる
6583
+ if ((items || []).some(isRetired)) opts.push(RETIRED_FILTER);
6571
6584
  if (withFollowup) {
6572
6585
  ["followup-done", "followup-open", "followup-none"].forEach(function (v) {
6573
6586
  if ((items || []).some(function (r) { return matchesFollowupFilter(r, v); })) opts.push(v);
@@ -6585,6 +6598,8 @@ ${calendarAddUrl.toString()}
6585
6598
  function matchesStatusFilter(rec, withFollowup) {
6586
6599
  if (!listState.status) return true;
6587
6600
  if (listState.status === "proposed") return hasProposals(rec);
6601
+ // 決定以外の一覧(タブを移っても絞り込みは残る)では絞らない——全件が消えないように
6602
+ if (listState.status === RETIRED_FILTER) return rec && rec._kind === "decision" ? isRetired(rec) : true;
6588
6603
  if (Object.prototype.hasOwnProperty.call(FOLLOWUP_FILTER_LABEL, listState.status)) return withFollowup ? matchesFollowupFilter(rec, listState.status) : true;
6589
6604
  return rec.rstatus === listState.status;
6590
6605
  }
@@ -6879,6 +6894,66 @@ ${calendarAddUrl.toString()}
6879
6894
  return false;
6880
6895
  }
6881
6896
 
6897
+ /* -------------------------------------------------------------------------
6898
+ * 置き換え(#699 F3・オーナー決定 A-1')
6899
+ * -------------------------------------------------------------------------
6900
+ * **逆引きはビルド側が組んである**(supersedes.ts=engine supersedes-index.mjs の写し)。
6901
+ * rec.supersededBy(置き換え先・新しい順)と rec.supersedesIds(置き換えた決定)は、無ければキーごと無い。
6902
+ * 🔴 古い側の superseded_by(夜間の精製が書く写し)は画面でも読まない。
6903
+ */
6904
+ var RETIRED_FILTER = "retired";
6905
+ /** もう有効でない(確認済みの置き換えが 1 本以上) */
6906
+ function isRetired(rec) {
6907
+ return !!(rec && rec.supersededBy && rec.supersededBy.retired);
6908
+ }
6909
+ /** 一覧・詳細の印。「その後」の印が同じ「置き換え済み」を出しているときは並べない */
6910
+ function retiredBadge(rec) {
6911
+ if (!isRetired(rec) || followupMarkOf(rec) === "superseded") return null;
6912
+ return el("span", { class: "badge retired", text: "置き換え済み" });
6913
+ }
6914
+ /** 決定 1 件の行。SITE にあれば題名で詳細へのリンク、無ければ ID だけ */
6915
+ function supersedeRow(id, date, unconfirmed) {
6916
+ var rec = byId[id];
6917
+ var li = el("li", { class: "supersede-row" });
6918
+ if (rec && rec._kind === "decision") {
6919
+ var a = el("a", { href: "?id=" + encodeURIComponent(id), text: rec.title });
6920
+ a.addEventListener("click", function (ev) {
6921
+ if (ev.defaultPrevented || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
6922
+ ev.preventDefault();
6923
+ go(id);
6924
+ });
6925
+ li.appendChild(a);
6926
+ }
6927
+ li.appendChild(el("span", { class: "supersede-id mono", text: (date ? date + " · " : "") + id }));
6928
+ if (unconfirmed) {
6929
+ li.appendChild(el("span", { class: "supersede-unconfirmed", title: "置き換えた側の決定が、まだ人の確認を経ていません", text: "未確認" }));
6930
+ }
6931
+ return li;
6932
+ }
6933
+ /** 詳細の節「置き換え先の決定」「置き換えた決定」。どちらも無ければ null(要素を作らない) */
6934
+ function supersedeSection(d) {
6935
+ var by = (d && d.supersededBy && d.supersededBy.by) || [];
6936
+ var reps = (d && d.supersedesIds) || [];
6937
+ if (!by.length && !reps.length) return null;
6938
+ var box = el("div", { class: "supersede-section" });
6939
+ if (by.length) {
6940
+ box.appendChild(el("div", { class: "section-label", text: "置き換え先の決定" }));
6941
+ var ul = el("ul", { class: "supersede-list supersede-by" });
6942
+ by.forEach(function (b) { ul.appendChild(supersedeRow(b.id, b.date, !b.confirmed)); });
6943
+ box.appendChild(ul);
6944
+ }
6945
+ if (reps.length) {
6946
+ box.appendChild(el("div", { class: "section-label", text: "置き換えた決定" }));
6947
+ var ul2 = el("ul", { class: "supersede-list supersede-replaces" });
6948
+ reps.forEach(function (id) {
6949
+ var r = byId[id];
6950
+ ul2.appendChild(supersedeRow(id, r && r._kind === "decision" ? r.date : "", false));
6951
+ });
6952
+ box.appendChild(ul2);
6953
+ }
6954
+ return box;
6955
+ }
6956
+
6882
6957
  // 未確認が1件も無い一覧では絞り込みを出さない
6883
6958
  function hasUnverified(items) {
6884
6959
  return (items || []).some(function (r) { return r.rstatus === "draft"; });
@@ -12216,7 +12291,7 @@ ${calendarAddUrl.toString()}
12216
12291
  "会議": { icon: "🎙", desc: "文字起こしから議事録を生成" },
12217
12292
  "共有資料": { icon: "📁", desc: "資料をMarkdown化して取り込み" },
12218
12293
  "チャット": { icon: "💬", desc: "チャンネルの会話をGoldの材料に" },
12219
- "デザイン": { icon: "🎨", desc: "画面インベントリの同期" },
12294
+ "デザイン": { icon: "🎨", desc: "Figmaのコメント(指摘)の取り込み" },
12220
12295
  "開発": { icon: "🐙", desc: "IssuesをGoldの材料に" },
12221
12296
  "ハーネス連携": { icon: "🧰", desc: "スキルカタログを開く(Claude Code で使う/定期実行にする)" },
12222
12297
  };
@@ -14924,15 +14999,16 @@ ${calendarAddUrl.toString()}
14924
14999
  var lines = [
14925
15000
  "「" + (f.name || f.key) + "」をデザイン同期の対象から外します。",
14926
15001
  "",
14927
- "次回の同期から、このファイルの画面は画面インベントリに現れなくなります。",
15002
+ "次回の同期から、このファイルのコメントは取り込まれなくなります。",
14928
15003
  ];
14929
15004
  // **最後の1件は結果が違う。** 同期は空の設定だと何もせずに終わるので、
14930
- // 取り込み済みの画面インベントリが消えずに残る(sync_designs.py は
15005
+ // 取り込み済みの分(デザイン/指摘/、移行前の案件は デザイン/inventory/)が
15006
+ // 消えずに残る(sync_designs.py は
14931
15007
  // 全再生成の手前で早期returnする)。ここで伝えないと放置される。
14932
15008
  if (figsLeft === 1) {
14933
15009
  lines.push(
14934
15010
  "",
14935
- "これが最後の1件です。外すとデザイン同期は何もしなくなりますが、すでに取り込んだ画面インベントリはリポジトリに残ります(不要なら手で削除してください)。",
15011
+ "これが最後の1件です。外すとデザイン同期は何もしなくなりますが、すでに取り込んだ分(デザイン/ の下)はリポジトリに残ります(不要なら手で削除してください)。",
14936
15012
  );
14937
15013
  }
14938
15014
  lines.push("", "外しますか?");
@@ -18033,6 +18109,7 @@ ${calendarAddUrl.toString()}
18033
18109
  draftBadge(d),
18034
18110
  proposalBadge(d),
18035
18111
  followupBadge(d),
18112
+ retiredBadge(d),
18036
18113
  latestBadge(d),
18037
18114
  el("span", { class: "mono", text: d.date || "----" }),
18038
18115
  catBadge(d.category),
@@ -18196,7 +18273,7 @@ ${calendarAddUrl.toString()}
18196
18273
  /**
18197
18274
  * 2 つの日時が同じ瞬間か。**印の行を決めるのに使う。**
18198
18275
  *
18199
- * 🔴 突き合わせるのは rec.rederived.changedAt(機械の書き直しの author 日付)と
18276
+ * 🔴 突き合わせるのは rec.rederived.changedAt(帳簿の rewritten.at=機械の書き直しの日時・#799)と
18200
18277
  * 変更履歴の at。**器にはコミットの sha が 1 つも入っていない**(evidence は
18201
18278
  * 根拠の**件数**であって sha ではない)ので、sha では突き合わせられない。
18202
18279
  * 綴りが違っても同じ瞬間なら当てる(どちらも git の author 日付だが、器の出どころが別)
@@ -18251,7 +18328,7 @@ ${calendarAddUrl.toString()}
18251
18328
  // 🔴 **この時点では、どのプロジェクトにもこの欄はまだ無い。** 無ければ節ごと出さない
18252
18329
  // (今までと同じに見えること)のが、この節のいちばん大事な性質。
18253
18330
  // 🔴 **画面の中では linkedIds / .linked-ids / .li-* と呼ぶ。** フロントマターの欄名の綴りは
18254
- // GoldCommitIdentity(帯のコミットの筆者)と同じ語なので、画面の名前には持ち込まない。
18331
+ // GoldCommitIdentity(帯のコミットの筆者・#799 で撤去)と同じ語だったので、画面の名前には持ち込まない。
18255
18332
  // 欄名を綴るのは FM_LINKED_IDS 1 本だけ(書く側も同じ定数を引く。正本は src/external-id.ts)
18256
18333
  var FM_LINKED_IDS = ${JSON.stringify(external_id_1.FM_LINKED_IDS)};
18257
18334
  // 道具の読める名前。🔴 **語彙の外の種類は、その綴りをそのまま出す**
@@ -19614,6 +19691,7 @@ ${calendarAddUrl.toString()}
19614
19691
  var meta = el("div", { class: "detail-meta" }, [
19615
19692
  draftBadge(d),
19616
19693
  proposalBadge(d),
19694
+ retiredBadge(d),
19617
19695
  latestBadge(d),
19618
19696
  el("span", { class: "mono", text: d.date || "----" }),
19619
19697
  catBadge(d.category),
@@ -19659,6 +19737,10 @@ ${calendarAddUrl.toString()}
19659
19737
  app.appendChild(ul);
19660
19738
  }
19661
19739
 
19740
+ // 置き換え(#699 F3)。本文・参照の後、「その後の記録」の前。置き換えの無い決定では何も作らない
19741
+ var sup = supersedeSection(d);
19742
+ if (sup) app.appendChild(sup);
19743
+
19662
19744
  appendRecordFooter(d);
19663
19745
  }
19664
19746
 
@@ -19671,9 +19753,11 @@ ${calendarAddUrl.toString()}
19671
19753
  }
19672
19754
 
19673
19755
  // ---------- エクスプローラー(Miller columns: Gold層から関係を辿るカラム表示) ----------
19756
+ // superseded_by は**辺としては描かない**(ビルド側の MIRROR_RELS)。ここに置くのは
19757
+ // レコード情報(frontmatter)のパネルが英語の語のまま出さないためだけ(凡例の REL_LABEL には足さない)
19674
19758
  var REL_LABEL = {
19675
19759
  based_on: "根拠", derived_from: "由来", relates_to: "関連",
19676
- supersedes: "置換", reference: "参照", source: "出典", link: "リンク",
19760
+ supersedes: "置換", superseded_by: "置換先", reference: "参照", source: "出典", link: "リンク",
19677
19761
  };
19678
19762
  var explorerState = { folder: null, path: [], q: "", sx: 0, autoRight: false };
19679
19763
  // 索引もグラフ本体と同じ材料(削除済みを除いたもの)から作る。別々に間引くと、
@@ -20000,6 +20084,10 @@ ${calendarAddUrl.toString()}
20000
20084
  badge.style.borderColor = REL_COLOR[r.rel] || "#cbd5e1";
20001
20085
  row.appendChild(badge);
20002
20086
  row.appendChild(fmTargetLink(String(r.target)));
20087
+ // 夜間処理が古い側に書く置換先の写しは、置き換えた側が未確認なら unconfirmed: true を持つ(#699 F3)
20088
+ if (r.rel === "superseded_by" && r.unconfirmed === true) {
20089
+ row.appendChild(el("span", { class: "supersede-unconfirmed", title: "置き換えた側の決定が、まだ人の確認を経ていません", text: "未確認" }));
20090
+ }
20003
20091
  cell.appendChild(row);
20004
20092
  });
20005
20093
  } else if (k === FM_LINKED_IDS && Array.isArray(v)) {
package/dist/repo.js CHANGED
@@ -7,7 +7,6 @@ exports.normalizeToHttps = normalizeToHttps;
7
7
  exports.classifyCommit = classifyCommit;
8
8
  exports.isFrontmatterKeyRename = isFrontmatterKeyRename;
9
9
  exports.collectAiChangeHistory = collectAiChangeHistory;
10
- exports.collectGoldCommitIdentities = collectGoldCommitIdentities;
11
10
  exports.readBlobsAt = readBlobsAt;
12
11
  exports.lastSubstantiveUpdate = lastSubstantiveUpdate;
13
12
  const node_child_process_1 = require("node:child_process");
@@ -761,81 +760,13 @@ const LAST_UPDATE_RENAME_OPT = "--find-renames=50%";
761
760
  * ここでは使わない——差分の取得にもう1回 git を呼ぶ割に、Home.md ではマイグレーションの
762
761
  * 件名規約で落ちる形しか観測されていない(適用しないと決めた・設計レビュー承認済み)。
763
762
  */
764
- /* ===========================================================================
765
- * レコードを触ったコミットの素性(帯「人が確認した版から変わりました」の材料・#665)
766
- * ===========================================================================
767
- * `.cortex/gold-confirmation.json` の `changed_by` は **sha だけ**を持つ。人が確認した
768
- * 版から「**機械が**書き直したか」を見分けるには、その sha の author・件名・本文
769
- *(`Operated-By:` trailer)が要る——帳簿には無いので git から取る。
770
- *
771
- * 🔴 **レコード数に依存しない 1 回の `git log`。** 「ビルド1回につき git は2回」の規律
772
- *(`collectAiChangeHistory`)に**3 回目**を足す形になるが、per-path で呼ばない点は保つ。
773
- * sha を並べて `git log --no-walk <sha…>` にしない——sha の数(実データで 54・多い案件では
774
- * 数百)でコマンドラインが伸び、ARG_MAX と「履歴に無い sha で log 全体が落ちる」に当たる。
775
- */
776
- /**
777
- * 素性を読むコミット数の上限。
778
- *
779
- * ビューアのビルドは **fetch-depth 2000** で checkout する(engine `build-viewer.yml`)ので、
780
- * それ以上遡っても届かない。窓の外・浅い clone の sha は**見分けられない=「不明」**に倒れ、
781
- * 帯は立たない(既定は不明・#432 裁定 5-7)。差分や本文を読まない軽い pass なので、
782
- * 履歴の窓とそろえて広く取る。
783
- */
784
- const GOLD_IDENTITY_MAX_COMMITS = 2000;
785
- /** 本文(`%B`)の前に置く印。件名の区切り(US)とは別の文字にする(件名にも本文にも現れない) */
786
- const RSEP = "";
787
- /**
788
- * レコードを触ったコミットの素性を sha で引ける表にする(新しい順に上限まで)。
789
- *
790
- * git が無い・リポジトリでない・浅い clone で届かない sha は**表に載らない**
791
- *(呼び出し側は「載っていない sha は不明」として扱うこと)。
792
- */
793
- function collectGoldCommitIdentities(cwd) {
794
- const out = gitLog(cwd, [
795
- "log",
796
- `-n${GOLD_IDENTITY_MAX_COMMITS}`,
797
- "--no-color",
798
- // 本文まで読むので、コミットの境目(NUL)と本文の始まり(RSEP)を別の印で固定する。
799
- // 件名に US が混ざっても壊れないよう、下で 5 つ目以降を件名として繋ぎ直す
800
- `--format=%x00%H%x1f%aI%x1f%an%x1f%ae%x1f%s%x1e%B`,
801
- "--",
802
- RECORD_PATHSPEC,
803
- ], "レコードを触ったコミットの素性");
804
- const map = new Map();
805
- // 🔴 **行で切らない(NUL で切る)。** 本文は複数行なので、1 行 1 コミットの読み方は使えない
806
- for (const chunk of out.split(NUL)) {
807
- if (!chunk)
808
- continue;
809
- const at = chunk.indexOf(RSEP);
810
- const head = at === -1 ? chunk : chunk.slice(0, at);
811
- // 本文の末尾の改行は git が付けるもので、trailer の判定(行頭 ^)には影響しない
812
- const body = at === -1 ? "" : chunk.slice(at + RSEP.length);
813
- const parts = head.split(US);
814
- const sha = parts[0] || "";
815
- if (!sha)
816
- continue;
817
- // 同じ sha が 2 度出ることは無いが、出ても新しい側(先に出たもの)を残す
818
- if (map.has(sha))
819
- continue;
820
- map.set(sha, {
821
- sha: sha,
822
- at: parts[1] || "",
823
- authorName: parts[2] || "",
824
- authorEmail: parts[3] || "",
825
- subject: parts.slice(4).join(US),
826
- body: body,
827
- });
828
- }
829
- return map;
830
- }
831
763
  /**
832
764
  * 指定のコミットのファイルの中身を**まとめて 1 回**読む(`git cat-file --batch`)。
833
765
  *
834
766
  * 🔴 **レコードごとにプロセスを起こさない。** 帯(印)の判定は「人が確認した版と今の版で
835
767
  * 説明文・本文が変わったか」を見るので、確認の基準コミットのレコードを読む必要がある。
836
768
  * ここで `git show` をレコードごとに叩くと、用語が 1400 件ある案件でビルドが 1400 回
837
- * git を起こす——`collectGoldCommitIdentities` と同じく「git の呼び出しはレコード数に
838
- * 依存しない」の規律を保つため、key を stdin に流し込む `--batch` を 1 回だけ使う。
769
+ * git を起こす——「git の呼び出しはレコード数に依存しない」の規律を保つため、key を stdin に流し込む `--batch` を 1 回だけ使う。
839
770
  *
840
771
  * @param keys `<commit>:<path>` の列(重複は畳む)。**空なら git を起こさない**。
841
772
  * 🔴 **改行を含む key は git に渡す前に落とす**(`--batch` の問い合わせは 1 行 1 件なので、