@takagaki/cortex-decisions-viewer 0.12.72 → 0.12.73
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 +62 -3
- package/dist/commit-classify.js +157 -0
- package/dist/rederivation.js +369 -0
- package/dist/render.js +126 -0
- package/dist/repo.js +68 -0
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -56,6 +56,7 @@ const gray_matter_1 = __importDefault(require("gray-matter"));
|
|
|
56
56
|
const marked_1 = require("marked");
|
|
57
57
|
const sanitize_html_1 = __importDefault(require("sanitize-html"));
|
|
58
58
|
const repo_1 = require("./repo");
|
|
59
|
+
const rederivation_1 = require("./rederivation");
|
|
59
60
|
// 直近のGold昇格で追加されたレコードのパス集合。レコード生成の各所から参照するのでモジュールスコープに置く
|
|
60
61
|
// (引数で引き回すと呼び出し側の改修が広範囲になる)。build() の冒頭で1回だけ解決する。
|
|
61
62
|
let LATEST_PATHS = new Set();
|
|
@@ -661,6 +662,28 @@ async function readFleetSources(repoRoot) {
|
|
|
661
662
|
}
|
|
662
663
|
return { externalSources, slackWorkspaces, internalSources, pipelines, capabilities, fleetGeneratedAt, secretSources, secretSpaces, checks, githubOwners, backlogTarget, tools };
|
|
663
664
|
}
|
|
665
|
+
/**
|
|
666
|
+
* 区画のレコードに帯を載せ、用語集の並びを活性度にする(#665 便 12b)。
|
|
667
|
+
*
|
|
668
|
+
* 🔴 **帳簿が無い案件では、渡された配列をそのまま返す**(帯のキーも付かない・並びも従来のまま)。
|
|
669
|
+
* 44 案件のうち帳簿を持つのは栓 `rederive` を入れた案件だけなので、ここが「何もしない」に
|
|
670
|
+
* 倒れることが**既定の経路**である。
|
|
671
|
+
*
|
|
672
|
+
* 🔴 **突き合わせはビルド時にここで閉じる**(`proposals` と同じ)。画面側でパスを組み立て直すと、
|
|
673
|
+
* `repoPathOf` が `editUrl` 由来なので **`repoBaseUrl` が無い案件で帯が黙って消える**。
|
|
674
|
+
*/
|
|
675
|
+
function attachRederived(records, recordsRel, ctx, nowIso) {
|
|
676
|
+
if (!ctx)
|
|
677
|
+
return records;
|
|
678
|
+
const pathOf = (r) => (recordsRel ? `${recordsRel}/${r.fileName}` : r.fileName);
|
|
679
|
+
for (const r of records) {
|
|
680
|
+
const band = (0, rederivation_1.rederivedBandOf)(pathOf(r), ctx.ledgers, ctx.identities, ctx.miss);
|
|
681
|
+
// **帯が立たないレコードにはキーごと付けない**(空の器を通さない)
|
|
682
|
+
if (band)
|
|
683
|
+
r.rederived = band;
|
|
684
|
+
}
|
|
685
|
+
return (0, rederivation_1.orderByActivity)(records, pathOf, ctx.ledgers, nowIso);
|
|
686
|
+
}
|
|
664
687
|
/**
|
|
665
688
|
* AIからの更新提案(`.cortex/gold-proposals.json`)を読む。
|
|
666
689
|
*
|
|
@@ -1875,6 +1898,20 @@ async function build(opts) {
|
|
|
1875
1898
|
// 同一プロセスで2回ビルドしても前回の値が残らないよう、毎回入れ替える
|
|
1876
1899
|
PROPOSALS_BY_PATH = await readGoldProposals(repoRoot);
|
|
1877
1900
|
PROPOSAL_PATHS_MATCHED = new Set();
|
|
1901
|
+
// 再導出の 3 帳簿(#665 便 12b)。**帳簿が無い案件では帯も並び替えも作らない**ので、
|
|
1902
|
+
// 読めたときだけ git の素性を取りに行く(帳簿を持たない 44 案件で git の呼び出しを増やさない)
|
|
1903
|
+
const ledgers = await (0, rederivation_1.readRederivationLedgers)(repoRoot);
|
|
1904
|
+
const rederive = ledgers.confirmation.size || ledgers.activity.size
|
|
1905
|
+
? {
|
|
1906
|
+
ledgers: ledgers,
|
|
1907
|
+
// 🔴 **`repoRoot` で走らせる**(`collectAiChangeHistory` の `process.cwd()` に倣わない)。
|
|
1908
|
+
// あちらは「リポジトリルートで実行すること」が前提の既存の制約だが、こちらは repoRoot を
|
|
1909
|
+
// 既に知っている(帳簿をそこから読んでいる)。Decisions ディレクトリから CLI を叩いた
|
|
1910
|
+
// ときも pathspec が当たり、帯が黙って消えない
|
|
1911
|
+
identities: ledgers.confirmation.size ? (0, repo_1.collectGoldCommitIdentities)(repoRoot) : new Map(),
|
|
1912
|
+
miss: { unresolved: new Set() },
|
|
1913
|
+
}
|
|
1914
|
+
: null;
|
|
1878
1915
|
const decisions = [];
|
|
1879
1916
|
for (const fileName of mdFiles) {
|
|
1880
1917
|
const raw = await node_fs_1.promises.readFile(path.join(dirAbs, fileName), "utf8");
|
|
@@ -1885,6 +1922,8 @@ async function build(opts) {
|
|
|
1885
1922
|
let homeGrounding;
|
|
1886
1923
|
let homeUpdatedAt;
|
|
1887
1924
|
const sections = [];
|
|
1925
|
+
// グラフに渡す区画(**並べ替える前**の配列を持つ。上の sections とレコードの実体は同じ)
|
|
1926
|
+
const graphSections = [];
|
|
1888
1927
|
if (knowledgeRoot) {
|
|
1889
1928
|
try {
|
|
1890
1929
|
const raw = await node_fs_1.promises.readFile(path.join(knowledgeRoot, "Home.md"), "utf8");
|
|
@@ -1915,12 +1954,22 @@ async function build(opts) {
|
|
|
1915
1954
|
continue;
|
|
1916
1955
|
const recordsDir = path.join(knowledgeRoot, e.name, "records");
|
|
1917
1956
|
const records = await loadGenericRecords(recordsDir, exclude, repo.baseUrl, repo.branch);
|
|
1957
|
+
// 「人が確認した版から変わりました」の帯と、用語集の並び(#665 便 12b)。
|
|
1958
|
+
// **帳簿を持たない案件では 1 件も付かず、並びもそのまま**(下の attachRederived)
|
|
1959
|
+
const ordered = attachRederived(records, toRepoRelative(recordsDir), rederive, opts.generatedAt);
|
|
1918
1960
|
// レコード0件でもタブは出す(空状態から「+AIで登録」できる導線になる)。
|
|
1919
1961
|
// records/ 自体が無いディレクトリ(その区画が未導入の案件)はタブにしない
|
|
1920
|
-
if (records.length || (await isDirectory(recordsDir)))
|
|
1921
|
-
sections.push({ name: e.name, records });
|
|
1962
|
+
if (records.length || (await isDirectory(recordsDir))) {
|
|
1963
|
+
sections.push({ name: e.name, records: ordered });
|
|
1964
|
+
// 🔴 **グラフには並べ替える前の配列を渡す**(実装レビュー)。`computeLayout` は
|
|
1965
|
+
// レコードの並びを入力に初期座標を決めるので、一覧の並びが活性度で動くと
|
|
1966
|
+
// **点と線は同じなのに地図の絵が毎晩組み変わる**(cortex-context で 1414/1414 の
|
|
1967
|
+
// 座標が動くのを実測)。並びが効くのは一覧だけ、という約束をここで固定する
|
|
1968
|
+
graphSections.push({ name: e.name, records: records });
|
|
1969
|
+
}
|
|
1922
1970
|
}
|
|
1923
1971
|
sections.sort((a, b) => a.name.localeCompare(b.name, "ja"));
|
|
1972
|
+
graphSections.sort((a, b) => a.name.localeCompare(b.name, "ja"));
|
|
1924
1973
|
}
|
|
1925
1974
|
// 新しい順(date降順→id降順)
|
|
1926
1975
|
decisions.sort((a, b) => {
|
|
@@ -1934,13 +1983,23 @@ async function build(opts) {
|
|
|
1934
1983
|
// **突き合わなかった提案の件数をビルドログに出す。** path が1文字違うだけで
|
|
1935
1984
|
// 画面から静かに消えるので、出さないと「提案が出ない」の原因に辿り着けない。
|
|
1936
1985
|
// 表示からは落とす(どのレコードの話か分からない提案は、出しても行動につながらない)
|
|
1986
|
+
// 🔴 **帯が静かに消える経路を 1 行残す(設計レビュー 🟡10)。** `changed_by` の sha が
|
|
1987
|
+
// git の窓(fetch-depth 2000)の外にあると、人か機械か見分けられない=帯が立たない。
|
|
1988
|
+
// 画面には出さない(利用者が動ける症状ではない)が、ログに無いと「帯が出ない」の原因を
|
|
1989
|
+
// 「夜間がまだ書いていない」と切り分けられない
|
|
1990
|
+
if (rederive && rederive.miss.unresolved.size) {
|
|
1991
|
+
const shas = [...rederive.miss.unresolved];
|
|
1992
|
+
console.warn(`gold-confirmation.json: 変更のコミット ${shas.length} 件を git の履歴で引けませんでした` +
|
|
1993
|
+
`(浅い clone か履歴の窓の外。人か機械か見分けられないので帯は出しません): ` +
|
|
1994
|
+
shas.slice(0, 3).map((s) => s.slice(0, 8)).join(", ") + (shas.length > 3 ? " ほか" : ""));
|
|
1995
|
+
}
|
|
1937
1996
|
const unmatched = [...PROPOSALS_BY_PATH.keys()].filter((k) => !PROPOSAL_PATHS_MATCHED.has(k));
|
|
1938
1997
|
if (unmatched.length) {
|
|
1939
1998
|
console.warn(`gold-proposals.json: レコードに突き合わない提案が ${unmatched.length} 件ありました(表示しません): ` +
|
|
1940
1999
|
unmatched.slice(0, 5).join(", ") + (unmatched.length > 5 ? " ほか" : ""));
|
|
1941
2000
|
}
|
|
1942
2001
|
const contentEntries = await scanContent(repoRoot, knowledgeRoot ? path.basename(knowledgeRoot) : null, repo.baseUrl, repo.branch);
|
|
1943
|
-
const graph = buildGraph(decisions, home,
|
|
2002
|
+
const graph = buildGraph(decisions, home, graphSections, contentEntries, targetUrls, repo.baseUrl, repo.branch, refs);
|
|
1944
2003
|
computeLayout(graph); // 各ノードに x/y を書き込む(クライアントはこれを初期位置に使いウォームアップ省略)
|
|
1945
2004
|
// データソースと自動化(fleet-status.json 由来)。無ければ undefined でセクション非表示。
|
|
1946
2005
|
const fleet = await readFleetSources(repoRoot);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* コミットを **人/機械/不明** の 3 値に分ける(#665 便 12b・#432 裁定 5-4 / 5-7)。
|
|
4
|
+
*
|
|
5
|
+
* ## 🔴 これは写しで、正本は engine にある
|
|
6
|
+
*
|
|
7
|
+
* 正本: cortex-engine `scripts/observe/commit-classify.mjs`。表(入力 → 3 値):
|
|
8
|
+
* cortex-engine `test/observe-commit-classify.test.mjs` の 12 行。
|
|
9
|
+
* **写すのはこの表で、実装を書き写すのではない**——同じ入力を両方に流して突き合わせる
|
|
10
|
+
* (`connection-state-keys.mjs` の写しと同じ流儀)。tools 側の表は
|
|
11
|
+
* `test/fixtures/commit-classify-contract.json` にあり、`commit-classify-parity.test.mjs`
|
|
12
|
+
* が全行を流す。**engine が判定を変えたらこの fixture を差し替える**(テストは書き換えない)。
|
|
13
|
+
*
|
|
14
|
+
* 🔴 **tools 側で規則を足さない。** 「再導出:」のような件名を足したくなっても、足す先は
|
|
15
|
+
* engine の正本(写しに足すと、正本と写しが逆向きにずれて契約テストが意味を失う)。
|
|
16
|
+
*
|
|
17
|
+
* ## なぜ `repo.ts` の `classifyCommit` と別に置くのか
|
|
18
|
+
*
|
|
19
|
+
* `repo.ts` のものは**別の問い**に答える 3 値(human / migration / ai)で、画面の
|
|
20
|
+
* 「AI による最近の変更」が使っている。あれを揃えるのは #646 の便で、この便では触らない
|
|
21
|
+
* ——揃えると 44 案件の既存の画面(履歴の節)が同時に変わる。ここは**新しい帯の条件だけ**が
|
|
22
|
+
* 使う判定で、**既定は「不明」**(証拠が無ければ倒さない)。
|
|
23
|
+
*
|
|
24
|
+
* ## 裁定(2026-09-17・#432 のコメント 5-4)
|
|
25
|
+
*
|
|
26
|
+
* > Claude Code 経由の push も「人が書いた」に数える(責任の所在で決める。人の名義で本人が
|
|
27
|
+
* > 結果を見て push している)。件数の閾値は設けない。「機械」に数えるのは**夜間処理・
|
|
28
|
+
* > マイグレーション・一括取り込みだけ**。
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.DELETE_SUBJECT_RE = exports.CONFIRM_SUBJECT_RE = exports.MIGRATION_REASON = void 0;
|
|
32
|
+
exports.looksLikeMigrationSubject = looksLikeMigrationSubject;
|
|
33
|
+
exports.operatedByKind = operatedByKind;
|
|
34
|
+
exports.classifyGoldCommit = classifyGoldCommit;
|
|
35
|
+
exports.isMachineRewrite = isMachineRewrite;
|
|
36
|
+
/**
|
|
37
|
+
* 機械の名義(**この 3 経路だけ**が機械)。`why` は `reason` にそのまま出る。
|
|
38
|
+
*
|
|
39
|
+
* 🔴 **`reason` の綴りは帯の条件が読む**(マイグレーションだけは帯を立てない・#665)。
|
|
40
|
+
* 綴りを変えるなら正本と fixture と `MIGRATION_REASON` を同時に見ること。
|
|
41
|
+
*/
|
|
42
|
+
const MACHINE_SUBJECTS = [
|
|
43
|
+
{ re: /^自動追記/, why: "夜間処理" },
|
|
44
|
+
{ re: /^自動生成/, why: "夜間処理" },
|
|
45
|
+
{ re: /^週次整理/, why: "夜間処理(週次)" },
|
|
46
|
+
{ re: /^(?:マイグレーション|migrate:|chore\(migrate\))/i, why: "マイグレーション" },
|
|
47
|
+
{ re: /^一括取り込み/, why: "一括取り込み" },
|
|
48
|
+
];
|
|
49
|
+
/** マイグレーションの `reason`。**帯を立てない側に倒す鍵の 1 枚目**(#665 決定表 4d) */
|
|
50
|
+
exports.MIGRATION_REASON = "マイグレーション";
|
|
51
|
+
/**
|
|
52
|
+
* 🔴 **「引っ越し」の綴りの 2 枚目**(`repo.ts:230` の写し・設計レビュー 🔴1)。
|
|
53
|
+
*
|
|
54
|
+
* 正本の `MACHINE_SUBJECTS` のマイグレーションの行は件名の**頭**を `マイグレーション` /
|
|
55
|
+
* `migrate:` / `chore(migrate)` で見るが、**艦隊の実際の件名は
|
|
56
|
+
* `エンジンマイグレーションを適用(schema_version更新)`** で、頭が「エンジン」なので
|
|
57
|
+
* 当たらない(実測: レコードを触った 127 件のうち 93 件が「機械の名義」の枝に落ち、
|
|
58
|
+
* engine の `isMigration()` では 0 件になる=`changed_by` から除かれていない)。
|
|
59
|
+
*
|
|
60
|
+
* この 1 枚が無いと、**引っ越しの翌朝に確認済みのレコード全件へ帯が出る**——実データの
|
|
61
|
+
* コミットの 53.6% が引っ越しで、中身は 1 文字も変わっていない。
|
|
62
|
+
*
|
|
63
|
+
* ⚠️ これは**正本に足すべき規則**(申し送り済み)。正本が拾うようになってもこの枚は
|
|
64
|
+
* 害にならない(両方が「引っ越し」と言うだけ)ので、**消すのは正本が実件名を拾ったあと**。
|
|
65
|
+
* 綴りは画面の「AI による最近の変更」が使う `repo.ts` の判定とそろえてある——ずれると
|
|
66
|
+
* 「履歴からは隠れているのに帯は立つ」という食い違った 1 ページができる(🔴7)。
|
|
67
|
+
*/
|
|
68
|
+
const MIGRATION_SUBJECT_RE = /^(エンジン)?マイグレーション|^スキーマ移行|^schema[_-]?version|^(apply\s+)?[a-z ]*migration/i;
|
|
69
|
+
/** 件名が「データ構造の引っ越し」の規約か(`repo.ts` の綴り) */
|
|
70
|
+
function looksLikeMigrationSubject(subject) {
|
|
71
|
+
return MIGRATION_SUBJECT_RE.test(String(subject || ""));
|
|
72
|
+
}
|
|
73
|
+
/** 機械の名義(名前)。`…[bot]` は GitHub のアプリ名義の綴り */
|
|
74
|
+
const MACHINE_NAME_RE = /(?:^|\s)(?:github-actions|dependabot|renovate)(?:\[bot\])?$|\[bot\]$/i;
|
|
75
|
+
/**
|
|
76
|
+
* 「人の名義とは言えない」メール。`[bot]@users.noreply.github.com` は機械の名義だが、
|
|
77
|
+
* `actions@github.com` のような汎用の名義は「誰が押したか分からない」=**不明**に倒す。
|
|
78
|
+
*/
|
|
79
|
+
const BOT_EMAIL_RE = /\[bot\]@users\.noreply\.github\.com$/i;
|
|
80
|
+
const ANONYMOUS_EMAIL_RE = /^(?:actions@github\.com|noreply@github\.com|no-reply@github\.com)$/i;
|
|
81
|
+
/** 人が Gold を直した印(tools の intake が書く件名。`repo.ts` と同じ綴り) */
|
|
82
|
+
exports.CONFIRM_SUBJECT_RE = /^Gold確認[::]/;
|
|
83
|
+
exports.DELETE_SUBJECT_RE = /^(?:レコード削除|Gold削除)/;
|
|
84
|
+
/** `Operated-By:` trailer の値から**経路だけ**を取る(値そのものは持ち回さない・PII) */
|
|
85
|
+
function operatedByKind(trailerValue) {
|
|
86
|
+
const v = String(trailerValue || "").trim();
|
|
87
|
+
if (!v)
|
|
88
|
+
return "none";
|
|
89
|
+
return /^basic:/.test(v) ? "basic" : "portal";
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* コミット 1 件を 3 値に分ける(正本と同じ順序・同じ規則)。
|
|
93
|
+
*
|
|
94
|
+
* 名前を `classifyCommit` にしない——`repo.ts` に同名で**別の問い**に答える関数があり、
|
|
95
|
+
* 取り違えると画面の履歴の節と帯の条件が静かに入れ替わる。
|
|
96
|
+
*/
|
|
97
|
+
function classifyGoldCommit({ authorName = "", authorEmail = "", committerName: _committerName = "", subject = "", body = "", } = {}) {
|
|
98
|
+
const trailer = (String(body).match(/^Operated-By:\s*(.+)$/m) || [])[1] || "";
|
|
99
|
+
const kind = operatedByKind(trailer);
|
|
100
|
+
// **検証済みか**は名乗り券の有無だけで決める(`basic:` は券が無いときの既定=自己申告)
|
|
101
|
+
const verified = kind === "portal";
|
|
102
|
+
const confirmer = (String(subject).match(exports.CONFIRM_SUBJECT_RE)
|
|
103
|
+
? String(subject).replace(exports.CONFIRM_SUBJECT_RE, "").trim()
|
|
104
|
+
: null) || null;
|
|
105
|
+
const out = (by, reason) => ({
|
|
106
|
+
by,
|
|
107
|
+
verified: by === "human" ? verified : false,
|
|
108
|
+
reason,
|
|
109
|
+
operatedByKind: kind,
|
|
110
|
+
confirmer,
|
|
111
|
+
});
|
|
112
|
+
// ---- 1. 機械(夜間処理・マイグレーション・一括取り込みだけ) ----
|
|
113
|
+
const machineSubject = MACHINE_SUBJECTS.find((m) => m.re.test(String(subject)));
|
|
114
|
+
if (machineSubject)
|
|
115
|
+
return out("machine", machineSubject.why);
|
|
116
|
+
if (MACHINE_NAME_RE.test(String(authorName).trim()))
|
|
117
|
+
return out("machine", "機械の名義");
|
|
118
|
+
if (BOT_EMAIL_RE.test(String(authorEmail).trim()))
|
|
119
|
+
return out("machine", "機械の名義(メール)");
|
|
120
|
+
// ---- 2. 人(**Claude Code 経由の push もここ**・裁定 5-4) ----
|
|
121
|
+
// 🔴 `Co-Authored-By: Claude` / `Generated with Claude Code` を**機械の証拠にしない**。
|
|
122
|
+
// 人の名義で本人が結果を見て push しているので、責任の所在は人にある
|
|
123
|
+
if (String(subject).match(exports.CONFIRM_SUBJECT_RE))
|
|
124
|
+
return out("human", "人が確認した(件名)");
|
|
125
|
+
if (exports.DELETE_SUBJECT_RE.test(String(subject)))
|
|
126
|
+
return out("human", "人が消した(件名)");
|
|
127
|
+
if (kind !== "none")
|
|
128
|
+
return out("human", "画面からの操作(Operated-By)");
|
|
129
|
+
const name = String(authorName).trim();
|
|
130
|
+
const email = String(authorEmail).trim();
|
|
131
|
+
if (name && email && !ANONYMOUS_EMAIL_RE.test(email))
|
|
132
|
+
return out("human", "人の名義の push");
|
|
133
|
+
// ---- 3. 既定は不明(裁定 5-7) ----
|
|
134
|
+
// 名義が空・汎用の名義(`actions@github.com`)は「誰が押したか分からない」
|
|
135
|
+
return out("unknown", "名義から人か機械か決められない");
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* **帯を立てる側の機械の書き直しか**(#665 決定表 4c・4d)。
|
|
139
|
+
*
|
|
140
|
+
* 機械の 3 経路(夜間処理・引っ越し・一括取り込み)のうち**引っ越しだけを外す**。実データの
|
|
141
|
+
* コミットの 53.6% が引っ越しで、中身は 1 文字も変わっていない(`repo.ts:199-207` の実測)
|
|
142
|
+
* ので、これで帯を立てると全レコードに帯が出る。engine も `changed_by` を作るときに
|
|
143
|
+
* `isMigration()` を当てているが(`history.mjs:145`)、**それは正本の綴りしか拾わない**うえに
|
|
144
|
+
* **古い帳簿・作り直し前の帳簿を読む夜がある**ので、写し側でも外す。
|
|
145
|
+
*
|
|
146
|
+
* 🔴 **判定は 2 枚の OR**(設計レビュー 🔴1): 正本の `reason` と `repo.ts` の綴り。
|
|
147
|
+
* **どちらかが「引っ越し」と言えば立てない**(片方だけに頼ると実件名が素通りする)。
|
|
148
|
+
*/
|
|
149
|
+
function isMachineRewrite(c, subject) {
|
|
150
|
+
if (c.by !== "machine")
|
|
151
|
+
return false;
|
|
152
|
+
if (c.reason === exports.MIGRATION_REASON)
|
|
153
|
+
return false;
|
|
154
|
+
if (looksLikeMigrationSubject(subject))
|
|
155
|
+
return false;
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
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.emptyLedgers = exports.LEDGER_VERSION = void 0;
|
|
7
|
+
exports.parseLedger = parseLedger;
|
|
8
|
+
exports.readRederivationLedgers = readRederivationLedgers;
|
|
9
|
+
exports.parseInjectedNow = parseInjectedNow;
|
|
10
|
+
exports.activityScore = activityScore;
|
|
11
|
+
exports.rederivedBandOf = rederivedBandOf;
|
|
12
|
+
exports.orderByActivity = orderByActivity;
|
|
13
|
+
/**
|
|
14
|
+
* 再導出の 3 帳簿を読み、帯「人が確認した版から変わりました」と用語集の並びを作る(#665 便 12b)。
|
|
15
|
+
*
|
|
16
|
+
* | 帳簿 | 何を持つか | 書き手 |
|
|
17
|
+
* | --- | --- | --- |
|
|
18
|
+
* | `.cortex/gold-confirmation.json` | レコードごとの「人が確認した版」と、そこから変わったか(**sha だけ**) | 夜間(全置換・git から作り直せる) |
|
|
19
|
+
* | `.cortex/gold-rederivation.json` | 書き直しの根拠の件数・shadow の「書くつもりだった差分」 | 夜間(追記・区画ごとの節) |
|
|
20
|
+
* | `.cortex/gold-activity.json` | 「最後に見た日」と触れた日の列(+観測の到達点 `cursor`) | 夜間(追記・剪定つき) |
|
|
21
|
+
*
|
|
22
|
+
* 形の正本は cortex-engine `scripts/rederive/ledgers.mjs` と `docs/cortex-derived-files.md`。
|
|
23
|
+
*
|
|
24
|
+
* ## 🔴 帳簿が無い・壊れている・古い形の案件は「今までと 1 バイトも同じ画面」
|
|
25
|
+
*
|
|
26
|
+
* 44 案件に配られる画面で、帳簿を持つのは栓 `rederive` を入れた案件だけ(既定オフ)。
|
|
27
|
+
* **読めないものは空**として正常に続ける(`readGoldProposals` と同じ規律)。空のときは
|
|
28
|
+
* 帯の器も並び替えも一切作らない——「知らないことは、キーが無いことで表す」。
|
|
29
|
+
*
|
|
30
|
+
* ## 🔴 生成時刻を持ち込まない・score を焼かない
|
|
31
|
+
*
|
|
32
|
+
* 活性度(順位)は**帳簿の日付の列からビルド時に計算する**(#432 問い 5-D)。時計は
|
|
33
|
+
* 引数で注入する(`generatedAt`)——`new Date()` を中で読むと、固定日付のテストが
|
|
34
|
+
* 実時間と比べられて**ある日から赤くなる**(作業規約「日付爆弾」)。
|
|
35
|
+
*/
|
|
36
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
37
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
38
|
+
const commit_classify_1 = require("./commit-classify");
|
|
39
|
+
/** 帳簿の版(違ったら空として扱う・engine `LEDGER_VERSION`) */
|
|
40
|
+
exports.LEDGER_VERSION = 1;
|
|
41
|
+
/** 帯を出す区画。**用語とメンバーだけ**(Decisions は積み上げ・Rules は便 13) */
|
|
42
|
+
const BAND_SECTION_RE = /^Cortex\/(?:Glossary|Members)\/records\/[^/]+\.md$/;
|
|
43
|
+
/** 並びを変える区画。**用語集だけ**(メンバーの並びは便 14) */
|
|
44
|
+
const ORDER_SECTION_RE = /^Cortex\/Glossary\/records\/[^/]+\.md$/;
|
|
45
|
+
/** ACT-R base-level の減衰(#432 裁定 5-5: d = 0.5) */
|
|
46
|
+
const ACTR_DECAY = 0.5;
|
|
47
|
+
/**
|
|
48
|
+
* 経過日数の下限(日)。**同じ日に触れられた行で 0 の −0.5 乗(=∞)を作らない。**
|
|
49
|
+
* 0.5 日=「今日触れられた」を半日前として数える(ACT-R の実装で普通に使う丸め)。
|
|
50
|
+
*/
|
|
51
|
+
const MIN_ELAPSED_DAYS = 0.5;
|
|
52
|
+
const emptyLedgers = () => ({
|
|
53
|
+
confirmation: new Map(),
|
|
54
|
+
rederivation: new Map(),
|
|
55
|
+
activity: new Map(),
|
|
56
|
+
});
|
|
57
|
+
exports.emptyLedgers = emptyLedgers;
|
|
58
|
+
/**
|
|
59
|
+
* `version` が違う・壊れている・オブジェクトでない → **null(空として扱って続ける)**。
|
|
60
|
+
* engine `parseLedger` と同じ判定。
|
|
61
|
+
*/
|
|
62
|
+
function parseLedger(text) {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(String(text || ""));
|
|
65
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
66
|
+
return null;
|
|
67
|
+
if (Number(parsed.version) !== exports.LEDGER_VERSION)
|
|
68
|
+
return null;
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** 1 本読む。無い(ENOENT)ことは警告しない(既定の状態)。壊れているときだけ 1 行残す */
|
|
76
|
+
async function readOne(repoRoot, file) {
|
|
77
|
+
let raw;
|
|
78
|
+
try {
|
|
79
|
+
raw = await promises_1.default.readFile(node_path_1.default.join(repoRoot, ".cortex", file), "utf8");
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
if (e?.code !== "ENOENT") {
|
|
83
|
+
console.warn(`${file} を読めませんでした(帯と並びは出さずに続けます): ${String(e)}`);
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const parsed = parseLedger(raw);
|
|
88
|
+
// **壊れている・版が違うときは 1 行残す。** 静かに 0 件になると、帯が出ない理由が
|
|
89
|
+
// 「夜間がまだ書いていない」のか「読めていない」のか切り分けられない
|
|
90
|
+
if (!parsed)
|
|
91
|
+
console.warn(`${file} の形式か version が想定と違います(帯と並びは出さずに続けます)`);
|
|
92
|
+
return parsed;
|
|
93
|
+
}
|
|
94
|
+
const asRecord = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
95
|
+
/**
|
|
96
|
+
* 3 帳簿を読む。**フィールドは明示で列挙し、素通しはしない**(`readGoldProposals` の流儀)——
|
|
97
|
+
* engine は単独で版を上げるので、画面が知らないキーが必ず先に入る。
|
|
98
|
+
*/
|
|
99
|
+
async function readRederivationLedgers(repoRoot) {
|
|
100
|
+
const out = (0, exports.emptyLedgers)();
|
|
101
|
+
const conf = await readOne(repoRoot, "gold-confirmation.json");
|
|
102
|
+
const records = asRecord(conf?.records);
|
|
103
|
+
if (records) {
|
|
104
|
+
for (const [p, v] of Object.entries(records)) {
|
|
105
|
+
const row = asRecord(v);
|
|
106
|
+
const confirmed = asRecord(row?.confirmed);
|
|
107
|
+
if (!confirmed)
|
|
108
|
+
continue;
|
|
109
|
+
// 🔴 **基準コミットが空の行も持つ**(実装レビュー)。帯は作れない(差分も戻すも
|
|
110
|
+
// 指す先が無い)が、**「人が確認した」という事実は並びが使う**——ここで落とすと、
|
|
111
|
+
// 確認済みなのに先頭群から黙って外れる。帯の側(`rederivedBandOf`)で落とす
|
|
112
|
+
const commit = confirmed.commit != null ? String(confirmed.commit) : "";
|
|
113
|
+
const changedBy = Array.isArray(row?.changed_by)
|
|
114
|
+
? row.changed_by.map((s) => String(s)).filter((s) => s !== "")
|
|
115
|
+
: [];
|
|
116
|
+
out.confirmation.set(p, {
|
|
117
|
+
at: confirmed.at != null ? String(confirmed.at) : "",
|
|
118
|
+
by: confirmed.by != null ? String(confirmed.by) : "",
|
|
119
|
+
verified: confirmed.verified === true,
|
|
120
|
+
commit: commit,
|
|
121
|
+
changedBy: changedBy,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const red = await readOne(repoRoot, "gold-rederivation.json");
|
|
126
|
+
const redSections = asRecord(red?.sections);
|
|
127
|
+
if (redSections) {
|
|
128
|
+
for (const section of Object.values(redSections)) {
|
|
129
|
+
const rows = asRecord(section);
|
|
130
|
+
if (!rows)
|
|
131
|
+
continue;
|
|
132
|
+
for (const [p, v] of Object.entries(rows)) {
|
|
133
|
+
const row = asRecord(v);
|
|
134
|
+
if (!row)
|
|
135
|
+
continue;
|
|
136
|
+
// 根拠の件数。**数でないものは null**(「0 件」と「知らない」を言い分ける)
|
|
137
|
+
const n = Number(row.evidence);
|
|
138
|
+
out.rederivation.set(p, {
|
|
139
|
+
evidence: row.evidence != null && Number.isFinite(n) && n >= 0 ? Math.floor(n) : null,
|
|
140
|
+
commit: row.commit != null ? String(row.commit) : "",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const act = await readOne(repoRoot, "gold-activity.json");
|
|
146
|
+
const actSections = asRecord(act?.sections);
|
|
147
|
+
if (actSections) {
|
|
148
|
+
for (const section of Object.values(actSections)) {
|
|
149
|
+
const rows = asRecord(section);
|
|
150
|
+
if (!rows)
|
|
151
|
+
continue;
|
|
152
|
+
for (const [p, v] of Object.entries(rows)) {
|
|
153
|
+
const row = asRecord(v);
|
|
154
|
+
if (!row)
|
|
155
|
+
continue;
|
|
156
|
+
const recent = Array.isArray(row.recent)
|
|
157
|
+
? row.recent.map((d) => String(d)).filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d))
|
|
158
|
+
: [];
|
|
159
|
+
const n = Number(row.n);
|
|
160
|
+
out.activity.set(p, {
|
|
161
|
+
lastSeen: row.last_seen != null ? String(row.last_seen) : "",
|
|
162
|
+
n: Number.isFinite(n) && n > 0 ? Math.floor(n) : 0,
|
|
163
|
+
recent: recent,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
/** `YYYY-MM-DD` を「その日の正午(UTC)」として読む(時刻を持たない日付に時差を持ち込まない) */
|
|
171
|
+
function dayMs(date) {
|
|
172
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
|
|
173
|
+
if (!m)
|
|
174
|
+
return null;
|
|
175
|
+
return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]), 12, 0, 0);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 注入された時計を epoch にする。
|
|
179
|
+
*
|
|
180
|
+
* 🔴 **ISO とは限らない**(実装レビュー)。`cli.ts` が作る既定の生成時刻は
|
|
181
|
+
* `2026-09-18 07:41 UTC` の形で、`Date.parse` が読めるのは V8 の非標準の親切に過ぎない。
|
|
182
|
+
* 同じ形を正規化する器が `pending-ops.ts` にもある(`pendingSiteTimeMs`)。
|
|
183
|
+
* ここでは**両方の形を明示で受ける**——読めない値は `null`(=並びを従来のままにする側)。
|
|
184
|
+
*/
|
|
185
|
+
function parseInjectedNow(nowIso) {
|
|
186
|
+
const raw = String(nowIso || "").trim();
|
|
187
|
+
if (!raw)
|
|
188
|
+
return null;
|
|
189
|
+
const direct = Date.parse(raw);
|
|
190
|
+
if (Number.isFinite(direct))
|
|
191
|
+
return direct;
|
|
192
|
+
// `YYYY-MM-DD HH:MM UTC` → `YYYY-MM-DDTHH:MMZ`
|
|
193
|
+
const m = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}(?::\d{2})?)\s*(?:UTC|Z)?$/.exec(raw);
|
|
194
|
+
if (!m)
|
|
195
|
+
return null;
|
|
196
|
+
const fixed = Date.parse(`${m[1]}T${m[2]}Z`);
|
|
197
|
+
return Number.isFinite(fixed) ? fixed : null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* 活性度(ACT-R base-level)**B = ln Σ_j t_j^(−0.5)**。
|
|
201
|
+
*
|
|
202
|
+
* `t_j` は触れた日から `nowIso`(注入した時計)までの経過日数。**score は帳簿に焼かない**
|
|
203
|
+
* ——式を変えたときに全件の再生成が要らないように、入力(日付の列)だけを持つ(裁定 5-5)。
|
|
204
|
+
*
|
|
205
|
+
* 引ける日付が 1 つも無ければ `null`(「活性度を知らない」=並びを従来のままにする側)。
|
|
206
|
+
*/
|
|
207
|
+
function activityScore(entry, nowIso) {
|
|
208
|
+
if (!entry)
|
|
209
|
+
return null;
|
|
210
|
+
const now = parseInjectedNow(nowIso);
|
|
211
|
+
if (now == null)
|
|
212
|
+
return null;
|
|
213
|
+
// 同じ日が 2 度入っていても 1 回だけ数える(engine 側でも重複は畳んでいる)
|
|
214
|
+
const days = [...new Set(entry.recent.length ? entry.recent : entry.lastSeen ? [entry.lastSeen] : [])];
|
|
215
|
+
let sum = 0;
|
|
216
|
+
for (const d of days) {
|
|
217
|
+
const ms = dayMs(d);
|
|
218
|
+
if (ms == null)
|
|
219
|
+
continue;
|
|
220
|
+
const elapsed = Math.max((now - ms) / 86400000, MIN_ELAPSED_DAYS);
|
|
221
|
+
sum += Math.pow(elapsed, -ACTR_DECAY);
|
|
222
|
+
}
|
|
223
|
+
if (sum <= 0)
|
|
224
|
+
return null;
|
|
225
|
+
const score = Math.log(sum);
|
|
226
|
+
// 🔴 **有限でない score は「知らない」に倒す**(設計レビュー 🔴6)。比較子が NaN を返すと
|
|
227
|
+
// `Array.sort` の順序は未定義で、並びが**呼ぶたびに変わる**(帳簿の壊れ方が並びの総崩れになる)。
|
|
228
|
+
// 未来の日付(時計のずれ・手で書いた帳簿)は上の下限で潰れるので、ここは最後の網
|
|
229
|
+
return Number.isFinite(score) ? score : null;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* そのレコードに帯を立てるか(#665 の**決定表**・上から順に評価し、当たった行で終わる)。
|
|
233
|
+
*
|
|
234
|
+
* | 順 | 条件 | 結果 |
|
|
235
|
+
* | --- | --- | --- |
|
|
236
|
+
* | 1 | path が用語・メンバーのレコードでない | 立てない |
|
|
237
|
+
* | 2 | `confirmation` に行が無い/基準コミットが空/確認日が日時として読めない | 立てない |
|
|
238
|
+
* | 3 | `changed_by` が空 | 立てない |
|
|
239
|
+
* | 4 | `changed_by` の 1 件ずつに 4a〜4e を当てて**残りが 0 件** | 立てない |
|
|
240
|
+
* | 4a | sha が基準コミットと同じ | その sha を落とす |
|
|
241
|
+
* | 4b | sha が git の窓に無い | 落とす(**不明**・`miss.unresolved` に数える) |
|
|
242
|
+
* | 4c | 分類が「人」または「不明」 | 落とす |
|
|
243
|
+
* | 4d | **どちらかの綴りで引っ越し** | 落とす |
|
|
244
|
+
* | 4e | author 日付が確認日**以前**(epoch 比較) | 落とす |
|
|
245
|
+
* | 5 | 残りのうち最も新しい author 日付を「書き直した日」にする | **立てる** |
|
|
246
|
+
* | 6 | 根拠は、帳簿の行の `commit` が `changed_by` に入っているときだけ出す | 出す/出さない |
|
|
247
|
+
* | 7 | 基準コミットが削除のコミットなら「戻す」を出さない(`baseDeleted`) | — |
|
|
248
|
+
*
|
|
249
|
+
* @param miss 立たなかった理由を数える器(呼び出し側で 1 つ作って全レコードで共有する)
|
|
250
|
+
*/
|
|
251
|
+
function rederivedBandOf(recordPath, ledgers, identities, miss) {
|
|
252
|
+
if (!BAND_SECTION_RE.test(recordPath))
|
|
253
|
+
return null;
|
|
254
|
+
const conf = ledgers.confirmation.get(recordPath);
|
|
255
|
+
// 基準コミットが空の行はここで落とす(読み手は並びのために持っている)
|
|
256
|
+
if (!conf || !conf.commit || !conf.changedBy.length)
|
|
257
|
+
return null;
|
|
258
|
+
// 確認日が読めない帳簿では「後か前か」を言えないので立てない(順序の確かめが要)
|
|
259
|
+
const confirmedMs = Date.parse(conf.at);
|
|
260
|
+
if (!Number.isFinite(confirmedMs))
|
|
261
|
+
return null;
|
|
262
|
+
let newest = null;
|
|
263
|
+
/** 確認より後の**人**のコミットの最新(帳簿が前の夜のもので、基準点がまだ動いていない夜) */
|
|
264
|
+
let newestHuman = 0;
|
|
265
|
+
for (const sha of conf.changedBy) {
|
|
266
|
+
// 4a. 基準コミット自身が並んでいる帳簿(作り直し前・手で書いたもの)に引っかからない
|
|
267
|
+
if (sha === conf.commit)
|
|
268
|
+
continue;
|
|
269
|
+
const id = identities.get(sha);
|
|
270
|
+
if (!id) {
|
|
271
|
+
// 4b. 窓の外・浅い clone = 見分けられない(不明)。**画面には出さずログに数える**
|
|
272
|
+
//(設計レビュー 🟡10: 帯が静かに消える経路がこれ)
|
|
273
|
+
if (miss)
|
|
274
|
+
miss.unresolved.add(sha);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
// 4e. 🔴 **確認より後であることを git の日付(epoch)で確かめる。** `changed_by` は engine が
|
|
278
|
+
// 「基準点より後」で作るが、帳簿は前の夜のものが残りうる(全置換に失敗した回・人が後から
|
|
279
|
+
// 確認して基準点が動いた回)。ここを外すと**確認より前の変更で帯が立つ**。
|
|
280
|
+
// ⚠️ 「後か」の真の正本は engine が見た git のトポロジ順(`history.mjs:141-145`)で、
|
|
281
|
+
// ここは**安全網**。author 日付は rebase・cherry-pick で前後しうるので、
|
|
282
|
+
// ずれたときは「立てない」側に倒れる(設計レビュー 🟡12)
|
|
283
|
+
const ms = Date.parse(id.at);
|
|
284
|
+
if (!Number.isFinite(ms) || ms <= confirmedMs)
|
|
285
|
+
continue;
|
|
286
|
+
// 4c・4d. 機械の書き直しだけを残す(件名は分類と同じものを渡す)
|
|
287
|
+
const classified = (0, commit_classify_1.classifyGoldCommit)({
|
|
288
|
+
authorName: id.authorName,
|
|
289
|
+
authorEmail: id.authorEmail,
|
|
290
|
+
subject: id.subject,
|
|
291
|
+
body: id.body,
|
|
292
|
+
});
|
|
293
|
+
if (!(0, commit_classify_1.isMachineRewrite)(classified, id.subject)) {
|
|
294
|
+
if (classified.by === "human" && ms > newestHuman)
|
|
295
|
+
newestHuman = ms;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (!newest || ms > newest.ms)
|
|
299
|
+
newest = { at: id.at, ms: ms };
|
|
300
|
+
}
|
|
301
|
+
if (!newest)
|
|
302
|
+
return null;
|
|
303
|
+
// 🔴 **人が最後に言ったなら立てない。** engine は基準点を「最後に人が触ったコミット」に
|
|
304
|
+
// 置くので、正しい帳簿の `changed_by` に人のコミットは入らない(`history.mjs:141`)。
|
|
305
|
+
// だが前の夜の帳簿が残った朝には「機械の書き直し → その後の人の編集」が並びうる。
|
|
306
|
+
// そこで帯を立てると、**「戻す」がその人の新しい編集ごと基準コミットへ戻す**——この機能で
|
|
307
|
+
// いちばん重い壊れ方なので、既定(立てない側)に倒す。engine が次の夜に基準点を動かす
|
|
308
|
+
if (newestHuman > newest.ms)
|
|
309
|
+
return null;
|
|
310
|
+
// 6. 根拠の件数は「この帯のコミットの記録」から出す(別の夜の数字を並べない)
|
|
311
|
+
const red = ledgers.rederivation.get(recordPath);
|
|
312
|
+
const evidence = red && red.evidence != null && red.commit && conf.changedBy.includes(red.commit)
|
|
313
|
+
? red.evidence
|
|
314
|
+
: null;
|
|
315
|
+
// 7. 基準コミットが削除のコミットか(窓の外なら分からないので false 側=リンクは出す。
|
|
316
|
+
// プロンプト側に「取れなかったときの逃げ道」を書いてある)
|
|
317
|
+
const base = identities.get(conf.commit);
|
|
318
|
+
const baseDeleted = base ? commit_classify_1.DELETE_SUBJECT_RE.test(base.subject) : false;
|
|
319
|
+
return {
|
|
320
|
+
confirmedAt: conf.at,
|
|
321
|
+
confirmedBy: conf.by,
|
|
322
|
+
confirmedVerified: conf.verified,
|
|
323
|
+
baseCommit: conf.commit,
|
|
324
|
+
changedAt: newest.at,
|
|
325
|
+
...(evidence != null ? { evidence: evidence } : {}),
|
|
326
|
+
...(baseDeleted ? { baseDeleted: true } : {}),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* 用語集の並び(#432 裁定 5-5)。**確認済みは先頭群・群の中は活性度の降順。**
|
|
331
|
+
*
|
|
332
|
+
* - 活性度を持つレコードが 1 件も無い区画は **1 バイトも触らない**(帳簿が無い案件は従来順)
|
|
333
|
+
* - 活性度を知らないレコードは群の末尾へ、**元の並びのまま**(安定)
|
|
334
|
+
* - 用語集だけ(メンバーの並びは便 14・Decisions と Rules は対象外)
|
|
335
|
+
*
|
|
336
|
+
* @param records 区画のレコード(`repoPathOf` で引けるパスを持つもの。並びは変えずに新しい配列を返す)
|
|
337
|
+
*/
|
|
338
|
+
function orderByActivity(records, pathOf, ledgers, nowIso) {
|
|
339
|
+
if (!records.length)
|
|
340
|
+
return records;
|
|
341
|
+
// この区画が並べ替えの対象か(用語集で、活性度の行が 1 件以上あるか)
|
|
342
|
+
const scored = records.map((r, i) => {
|
|
343
|
+
const p = pathOf(r);
|
|
344
|
+
const inSection = ORDER_SECTION_RE.test(p);
|
|
345
|
+
return {
|
|
346
|
+
r: r,
|
|
347
|
+
i: i,
|
|
348
|
+
confirmed: inSection && ledgers.confirmation.has(p),
|
|
349
|
+
score: inSection ? activityScore(ledgers.activity.get(p), nowIso) : null,
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
if (!scored.some((s) => s.score != null))
|
|
353
|
+
return records;
|
|
354
|
+
return scored
|
|
355
|
+
.slice()
|
|
356
|
+
.sort((a, b) => {
|
|
357
|
+
// 1. 確認済みは先頭群(群の境目は活性度より強い)
|
|
358
|
+
if (a.confirmed !== b.confirmed)
|
|
359
|
+
return a.confirmed ? -1 : 1;
|
|
360
|
+
// 2. 群の中は活性度の降順。知らないものは群の末尾
|
|
361
|
+
if (a.score != null && b.score != null && a.score !== b.score)
|
|
362
|
+
return b.score - a.score;
|
|
363
|
+
if ((a.score == null) !== (b.score == null))
|
|
364
|
+
return a.score == null ? 1 : -1;
|
|
365
|
+
// 3. 同点は元の並び(安定・日付降順 → タイトル昇順がそのまま残る)
|
|
366
|
+
return a.i - b.i;
|
|
367
|
+
})
|
|
368
|
+
.map((s) => s.r);
|
|
369
|
+
}
|
package/dist/render.js
CHANGED
|
@@ -4666,6 +4666,34 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
|
|
|
4666
4666
|
.promote-btn:disabled { opacity: .6; cursor: default; }
|
|
4667
4667
|
/* 何を上げようとしているか(リポジトリ上のパス)。リンクではないので控えめな色にする */
|
|
4668
4668
|
.promote-target { font-size: 12.5px; word-break: break-all; color: var(--muted); margin: 0 0 12px; }
|
|
4669
|
+
/* ---- 人が確認した版から変わりました(#665 便 12b)----
|
|
4670
|
+
🔴 **警告の色(黄・赤)にしない。** 機械が書き直したことは事故ではなく設計どおりの動き
|
|
4671
|
+
(用語と役割が話に出た翌朝に現実へ追随する)。裁定 5-1 の「文言に評価語なし」を色にも通し、
|
|
4672
|
+
下書きの帯(.draft-note の黄)・未記入の帯(.home-unfilled の赤)と**見た目で役割を分ける**。
|
|
4673
|
+
帯は症状の 1 行・その下に判断に要る値・一手は右に寄せる(2026-09-02 の規則) */
|
|
4674
|
+
.rederived-note {
|
|
4675
|
+
display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center;
|
|
4676
|
+
background: #eff6ff; border: 1px solid #93c5fd; border-radius: 8px;
|
|
4677
|
+
padding: 10px 14px; margin: 12px 0 4px; line-height: 1.7;
|
|
4678
|
+
}
|
|
4679
|
+
.rederived-body { flex: 1 1 320px; min-width: 0; }
|
|
4680
|
+
.rederived-title { font-size: 14.5px; font-weight: 700; color: #1e40af; }
|
|
4681
|
+
.rederived-meta { font-size: 13px; color: #1d4ed8; }
|
|
4682
|
+
.rederived-actions {
|
|
4683
|
+
flex: 0 0 auto; margin-left: auto;
|
|
4684
|
+
display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
|
|
4685
|
+
}
|
|
4686
|
+
.rederived-btn {
|
|
4687
|
+
flex: 0 0 auto; white-space: nowrap; text-decoration: none; cursor: pointer;
|
|
4688
|
+
padding: 6px 14px; border-radius: 999px; border: 1px solid #1d4ed8;
|
|
4689
|
+
background: #fff; color: #1d4ed8; font-size: 13px; font-weight: 600;
|
|
4690
|
+
}
|
|
4691
|
+
.rederived-btn:hover { background: #dbeafe; }
|
|
4692
|
+
/* 戻す(ディープリンク)は「押すと何かが起きる」側なので塗る */
|
|
4693
|
+
.rederived-btn-back { background: #1d4ed8; color: #fff; }
|
|
4694
|
+
.rederived-btn-back:hover { background: #1e40af; color: #fff; }
|
|
4695
|
+
/* リンクを作れない案件(リポジトリのURLが無い)では、どの版かを短縮SHAで残す */
|
|
4696
|
+
.rederived-base { font-size: 12.5px; color: var(--muted); }
|
|
4669
4697
|
/* ---- Home(案件の自己記述)---- */
|
|
4670
4698
|
/* 未記入の警告。**画面でいちばん強い赤にする**——27案件中24案件が未記入のまま
|
|
4671
4699
|
毎晩のAI生成が走っている状態で、控えめに出しても読まれない */
|
|
@@ -6195,6 +6223,99 @@ ${calendarAddUrl.toString()}
|
|
|
6195
6223
|
return note;
|
|
6196
6224
|
}
|
|
6197
6225
|
|
|
6226
|
+
/* -------------------------------------------------------------------------
|
|
6227
|
+
* 人が確認した版から変わりました(#665 便 12b・.cortex/ の 3 帳簿由来)
|
|
6228
|
+
* -------------------------------------------------------------------------
|
|
6229
|
+
* 機械(夜間の再導出)が、人が確認したあとでこのレコードを書き直したときだけ出す帯。
|
|
6230
|
+
* **条件はビルド側が決めてある**(rederivation.ts の決定表)ので、画面は
|
|
6231
|
+
* rec.rederived の**有無だけ**を見る——画面側でパスを組み立て直して判定すると、
|
|
6232
|
+
* リポジトリのURLが無い案件で帯が黙って消える(設計レビュー 🔴4)。
|
|
6233
|
+
*
|
|
6234
|
+
* 🔴 **評価語を書かない**(裁定 5-1)。「勝手に」「注意」は書かず、症状・日付・件数と
|
|
6235
|
+
* 一手だけを出す。説明(なぜ毎晩変わるのか)は書かない——同じページの
|
|
6236
|
+
* 「AI による最近の変更」が変わった行そのものを持っている。
|
|
6237
|
+
*/
|
|
6238
|
+
|
|
6239
|
+
/** 帯に出す 1 行。券の無い確認(自己申告)は名前を権威として出さない(#432 5-A) */
|
|
6240
|
+
function rederivedMetaText(b) {
|
|
6241
|
+
var parts = [];
|
|
6242
|
+
var who = b.confirmedBy
|
|
6243
|
+
? "(" + b.confirmedBy + (b.confirmedVerified ? "" : "・自己申告") + ")"
|
|
6244
|
+
: "";
|
|
6245
|
+
parts.push("確認 " + ahDate(b.confirmedAt) + who);
|
|
6246
|
+
parts.push("書き直し " + ahDate(b.changedAt));
|
|
6247
|
+
// **「根拠」は言えるときだけ**(この帯のコミットの記録が帳簿に無ければ出さない)
|
|
6248
|
+
if (typeof b.evidence === "number") parts.push("根拠 " + b.evidence + " 件");
|
|
6249
|
+
return parts.join("/");
|
|
6250
|
+
}
|
|
6251
|
+
/** ブランチ名を URL に入れる(release/x のような / を %2F に化けさせない) */
|
|
6252
|
+
function refForUrl(ref) {
|
|
6253
|
+
return encodeURIComponent(String(ref || "")).split("%2F").join("/");
|
|
6254
|
+
}
|
|
6255
|
+
/**
|
|
6256
|
+
* 確認した版との差分(GitHub の compare・基準コミット…HEAD)。
|
|
6257
|
+
*
|
|
6258
|
+
* ⚠️ compare は**リポジトリ全体**の比較で、このレコードだけの差分に絞る URL は GitHub に無い。
|
|
6259
|
+
* レコード単位の「変わった行」は同じページの「AI による最近の変更」が持つので、帯は
|
|
6260
|
+
* 「確認した版から今までの間に何があったか」への入口に留める(決定ログに覆し方あり)。
|
|
6261
|
+
*/
|
|
6262
|
+
function rederivedDiffEl(b) {
|
|
6263
|
+
if (!SITE.repoBaseUrl) {
|
|
6264
|
+
var short = b.baseCommit.length > 8 ? b.baseCommit.slice(0, 8) : b.baseCommit;
|
|
6265
|
+
return el("span", { class: "rederived-base", text: "確認した版 " + short });
|
|
6266
|
+
}
|
|
6267
|
+
var href = SITE.repoBaseUrl + "/compare/" + refForUrl(b.baseCommit) + "..." + refForUrl(SITE.branch || "HEAD");
|
|
6268
|
+
return el("a", {
|
|
6269
|
+
class: "rederived-btn", href: href, target: "_blank", rel: "noopener",
|
|
6270
|
+
text: "確認した版との差分",
|
|
6271
|
+
});
|
|
6272
|
+
}
|
|
6273
|
+
/**
|
|
6274
|
+
* 確認した版に戻す(claude.ai/code のディープリンク・裁定 5-2「v1 はディープリンク」)。
|
|
6275
|
+
*
|
|
6276
|
+
* **出さない場合が 3 つある**: 基準コミットが「レコード削除」のコミット(そのコミットに
|
|
6277
|
+
* この path が無く空振りする)・ディープリンクを組めない(リポジトリのURLが無い)・
|
|
6278
|
+
* 対象のパスが引けない。押せない口を出さない。
|
|
6279
|
+
*
|
|
6280
|
+
* 🔴 プロンプトに**人の名前を入れない**(confirmed.by は件名由来の任意文字列)。
|
|
6281
|
+
* 🔴 パスは**引用符でくるむ**(用語のファイル名には空白が入る。実データで 5 件)。
|
|
6282
|
+
* 🔴 コミットが手元に無いとき(浅い clone)の逃げ道を 2 段で書く(設計レビュー 🔴8)。
|
|
6283
|
+
*/
|
|
6284
|
+
function rederivedRestoreEl(rec, b) {
|
|
6285
|
+
if (b.baseDeleted) return null;
|
|
6286
|
+
if (!REPO_SLUG) return null;
|
|
6287
|
+
var p = repoPathOf(rec);
|
|
6288
|
+
if (!p) return null;
|
|
6289
|
+
var prompt = [
|
|
6290
|
+
"このレコードを、人が確認した版の内容に戻してください。",
|
|
6291
|
+
"対象のファイル: " + p,
|
|
6292
|
+
"確認した版のコミット: " + b.baseCommit,
|
|
6293
|
+
'git checkout ' + b.baseCommit + ' -- "' + p + '" を実行してください(ファイル名に空白が入ることがあるので引用符を外さないでください)。',
|
|
6294
|
+
"そのコミットが手元に無い(bad object と出る)ときは、git fetch origin " + b.baseCommit + " を先に実行してください。それでも取れないときは、GitHub でそのコミットのこのファイルを開き、本文を写してください。",
|
|
6295
|
+
"ほかのファイルは 1 つも触らないでください。",
|
|
6296
|
+
"書き込む前に、戻したあとの内容を提示して承認を得てください。",
|
|
6297
|
+
].join("\\n");
|
|
6298
|
+
return aiLink("rederived-btn rederived-btn-back", prompt, "確認した版に戻す");
|
|
6299
|
+
}
|
|
6300
|
+
/** 詳細ページの帯。rec.rederived が無いレコードでは **1 要素も作らない** */
|
|
6301
|
+
function rederivedNote(rec) {
|
|
6302
|
+
var b = rec && rec.rederived;
|
|
6303
|
+
if (!b) return null;
|
|
6304
|
+
var note = el("div", { class: "rederived-note" }, [
|
|
6305
|
+
el("div", { class: "rederived-body" }, [
|
|
6306
|
+
el("div", { class: "rederived-title", text: "人が確認した版から変わりました" }),
|
|
6307
|
+
el("div", { class: "rederived-meta", text: rederivedMetaText(b) }),
|
|
6308
|
+
]),
|
|
6309
|
+
]);
|
|
6310
|
+
var actions = el("div", { class: "rederived-actions" });
|
|
6311
|
+
var diff = rederivedDiffEl(b);
|
|
6312
|
+
if (diff) actions.appendChild(diff);
|
|
6313
|
+
var back = rederivedRestoreEl(rec, b);
|
|
6314
|
+
if (back) actions.appendChild(back);
|
|
6315
|
+
if (actions.children.length) note.appendChild(actions);
|
|
6316
|
+
return note;
|
|
6317
|
+
}
|
|
6318
|
+
|
|
6198
6319
|
// ---------- Gold の確認済み昇格(status: draft → active) ----------
|
|
6199
6320
|
//
|
|
6200
6321
|
// 内容がそのまま正しいとき、**編集せずに所有権だけ引き取る**ための最短経路。
|
|
@@ -17446,6 +17567,11 @@ ${calendarAddUrl.toString()}
|
|
|
17446
17567
|
var dn2 = draftNote(r);
|
|
17447
17568
|
if (dn2) app.appendChild(dn2);
|
|
17448
17569
|
|
|
17570
|
+
// 人が確認した版から機械が書き直したときの帯(#665)。**下書きの帯の次・提案の前**
|
|
17571
|
+
// ——「この記録は誰のものか」の話をまとめ、根拠の一覧(提案)より前に置く
|
|
17572
|
+
var rd2 = rederivedNote(r);
|
|
17573
|
+
if (rd2) app.appendChild(rd2);
|
|
17574
|
+
|
|
17449
17575
|
var pr2 = proposalsSection(r, r._section);
|
|
17450
17576
|
if (pr2) app.appendChild(pr2);
|
|
17451
17577
|
|
package/dist/repo.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.normalizeToHttps = normalizeToHttps;
|
|
|
7
7
|
exports.classifyCommit = classifyCommit;
|
|
8
8
|
exports.isFrontmatterKeyRename = isFrontmatterKeyRename;
|
|
9
9
|
exports.collectAiChangeHistory = collectAiChangeHistory;
|
|
10
|
+
exports.collectGoldCommitIdentities = collectGoldCommitIdentities;
|
|
10
11
|
exports.lastSubstantiveUpdate = lastSubstantiveUpdate;
|
|
11
12
|
const node_child_process_1 = require("node:child_process");
|
|
12
13
|
/**
|
|
@@ -757,6 +758,73 @@ const LAST_UPDATE_RENAME_OPT = "--find-renames=50%";
|
|
|
757
758
|
* ここでは使わない——差分の取得にもう1回 git を呼ぶ割に、Home.md ではマイグレーションの
|
|
758
759
|
* 件名規約で落ちる形しか観測されていない(適用しないと決めた・設計レビュー承認済み)。
|
|
759
760
|
*/
|
|
761
|
+
/* ===========================================================================
|
|
762
|
+
* レコードを触ったコミットの素性(帯「人が確認した版から変わりました」の材料・#665)
|
|
763
|
+
* ===========================================================================
|
|
764
|
+
* `.cortex/gold-confirmation.json` の `changed_by` は **sha だけ**を持つ。人が確認した
|
|
765
|
+
* 版から「**機械が**書き直したか」を見分けるには、その sha の author・件名・本文
|
|
766
|
+
*(`Operated-By:` trailer)が要る——帳簿には無いので git から取る。
|
|
767
|
+
*
|
|
768
|
+
* 🔴 **レコード数に依存しない 1 回の `git log`。** 「ビルド1回につき git は2回」の規律
|
|
769
|
+
*(`collectAiChangeHistory`)に**3 回目**を足す形になるが、per-path で呼ばない点は保つ。
|
|
770
|
+
* sha を並べて `git log --no-walk <sha…>` にしない——sha の数(実データで 54・多い案件では
|
|
771
|
+
* 数百)でコマンドラインが伸び、ARG_MAX と「履歴に無い sha で log 全体が落ちる」に当たる。
|
|
772
|
+
*/
|
|
773
|
+
/**
|
|
774
|
+
* 素性を読むコミット数の上限。
|
|
775
|
+
*
|
|
776
|
+
* ビューアのビルドは **fetch-depth 2000** で checkout する(engine `build-viewer.yml`)ので、
|
|
777
|
+
* それ以上遡っても届かない。窓の外・浅い clone の sha は**見分けられない=「不明」**に倒れ、
|
|
778
|
+
* 帯は立たない(既定は不明・#432 裁定 5-7)。差分や本文を読まない軽い pass なので、
|
|
779
|
+
* 履歴の窓とそろえて広く取る。
|
|
780
|
+
*/
|
|
781
|
+
const GOLD_IDENTITY_MAX_COMMITS = 2000;
|
|
782
|
+
/** 本文(`%B`)の前に置く印。件名の区切り(US)とは別の文字にする(件名にも本文にも現れない) */
|
|
783
|
+
const RSEP = "";
|
|
784
|
+
/**
|
|
785
|
+
* レコードを触ったコミットの素性を sha で引ける表にする(新しい順に上限まで)。
|
|
786
|
+
*
|
|
787
|
+
* git が無い・リポジトリでない・浅い clone で届かない sha は**表に載らない**
|
|
788
|
+
*(呼び出し側は「載っていない sha は不明」として扱うこと)。
|
|
789
|
+
*/
|
|
790
|
+
function collectGoldCommitIdentities(cwd) {
|
|
791
|
+
const out = gitLog(cwd, [
|
|
792
|
+
"log",
|
|
793
|
+
`-n${GOLD_IDENTITY_MAX_COMMITS}`,
|
|
794
|
+
"--no-color",
|
|
795
|
+
// 本文まで読むので、コミットの境目(NUL)と本文の始まり(RSEP)を別の印で固定する。
|
|
796
|
+
// 件名に US が混ざっても壊れないよう、下で 5 つ目以降を件名として繋ぎ直す
|
|
797
|
+
`--format=%x00%H%x1f%aI%x1f%an%x1f%ae%x1f%s%x1e%B`,
|
|
798
|
+
"--",
|
|
799
|
+
RECORD_PATHSPEC,
|
|
800
|
+
], "レコードを触ったコミットの素性");
|
|
801
|
+
const map = new Map();
|
|
802
|
+
// 🔴 **行で切らない(NUL で切る)。** 本文は複数行なので、1 行 1 コミットの読み方は使えない
|
|
803
|
+
for (const chunk of out.split(NUL)) {
|
|
804
|
+
if (!chunk)
|
|
805
|
+
continue;
|
|
806
|
+
const at = chunk.indexOf(RSEP);
|
|
807
|
+
const head = at === -1 ? chunk : chunk.slice(0, at);
|
|
808
|
+
// 本文の末尾の改行は git が付けるもので、trailer の判定(行頭 ^)には影響しない
|
|
809
|
+
const body = at === -1 ? "" : chunk.slice(at + RSEP.length);
|
|
810
|
+
const parts = head.split(US);
|
|
811
|
+
const sha = parts[0] || "";
|
|
812
|
+
if (!sha)
|
|
813
|
+
continue;
|
|
814
|
+
// 同じ sha が 2 度出ることは無いが、出ても新しい側(先に出たもの)を残す
|
|
815
|
+
if (map.has(sha))
|
|
816
|
+
continue;
|
|
817
|
+
map.set(sha, {
|
|
818
|
+
sha: sha,
|
|
819
|
+
at: parts[1] || "",
|
|
820
|
+
authorName: parts[2] || "",
|
|
821
|
+
authorEmail: parts[3] || "",
|
|
822
|
+
subject: parts.slice(4).join(US),
|
|
823
|
+
body: body,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
return map;
|
|
827
|
+
}
|
|
760
828
|
function lastSubstantiveUpdate(cwd, repoPath) {
|
|
761
829
|
// 🔴 **浅い clone を最初に弾く。** 浅い clone では境界コミットが親を持たないため、
|
|
762
830
|
// このファイルを触っていない直近のコミットまで `A <path>` として現れる(実測)。
|
package/package.json
CHANGED