@takagaki/cortex-decisions-viewer 0.12.78 → 0.12.79
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 +41 -3
- package/dist/external-id.js +37 -1
- package/dist/render.js +301 -74
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -93,6 +93,7 @@ function takeProposals(filePath) {
|
|
|
93
93
|
PROPOSAL_PATHS_MATCHED.add(filePath);
|
|
94
94
|
return hit;
|
|
95
95
|
}
|
|
96
|
+
const external_id_1 = require("./external-id");
|
|
96
97
|
const render_1 = require("./render");
|
|
97
98
|
/**
|
|
98
99
|
* この画面を作ったパッケージの版。**package.json を唯一の出どころにする。**
|
|
@@ -216,6 +217,17 @@ function toSearchText(md) {
|
|
|
216
217
|
.replace(/\s+/g, " ")
|
|
217
218
|
.trim();
|
|
218
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* 関係者の記録だけ、検索用の plaintext の後ろに「つながっている ID」の値と email を足す(#704)。
|
|
222
|
+
* **関係者以外の記録は 1 文字も変えない**(本文から作った searchText をそのまま返す)。
|
|
223
|
+
* 足す語の正本は external-id.ts の memberSearchTerms。
|
|
224
|
+
*/
|
|
225
|
+
function memberSearchText(data, sectionDir, base) {
|
|
226
|
+
if (!(0, external_id_1.isMemberRecord)(data.type, data.id, sectionDir))
|
|
227
|
+
return base;
|
|
228
|
+
const extra = (0, external_id_1.memberSearchTerms)(data);
|
|
229
|
+
return extra.length ? (base ? base + " " : "") + extra.join(" ") : base;
|
|
230
|
+
}
|
|
219
231
|
/** dir未指定時の探索候補(新構成 → 旧構成の順) */
|
|
220
232
|
const DEFAULT_DIR_CANDIDATES = ["Cortex/Decisions/records", "Cortex/Decisions", "Decisions"];
|
|
221
233
|
/**
|
|
@@ -762,6 +774,10 @@ readBlobs = repo_1.readBlobsAt) {
|
|
|
762
774
|
* **無い・壊れているときは提案ゼロで正常継続する。** 提案はあくまで付加情報で、
|
|
763
775
|
* これが読めないことでレコードの表示まで止めるのは釣り合わない。
|
|
764
776
|
*/
|
|
777
|
+
/** 文字列で中身があるときだけ返す(数・オブジェクトを String() して載せない) */
|
|
778
|
+
function optionalText(v) {
|
|
779
|
+
return typeof v === "string" && v.trim() !== "" ? v : "";
|
|
780
|
+
}
|
|
765
781
|
async function readGoldProposals(repoRoot) {
|
|
766
782
|
const empty = new Map();
|
|
767
783
|
let json;
|
|
@@ -827,6 +843,25 @@ async function readGoldProposals(repoRoot) {
|
|
|
827
843
|
reason: row.reason != null ? String(row.reason) : "",
|
|
828
844
|
createdAt: row.createdAt != null ? String(row.createdAt) : "",
|
|
829
845
|
};
|
|
846
|
+
// 名寄せの提案にだけ付く欄(engine の便 A・#704)。**文字列で中身があるときだけ**載せる
|
|
847
|
+
// ——無い提案にキーを生やすと、他の提案まで形が変わる(許可リストの守りのテストが見ている)。
|
|
848
|
+
// 🔴 **根拠の url(「その発言を開く」)はまだ通さない**(決定ログ 20260920-686.md の
|
|
849
|
+
// 「提案カードに新しい欄を 1 つも通さない」の url の項は生きている。20260923-704.md)
|
|
850
|
+
const kind = optionalText(row.kind);
|
|
851
|
+
if (kind)
|
|
852
|
+
proposal.kind = kind;
|
|
853
|
+
const claimKey = optionalText(row.claim_key);
|
|
854
|
+
if (claimKey)
|
|
855
|
+
proposal.claim_key = claimKey;
|
|
856
|
+
const displayName = optionalText(row.display_name);
|
|
857
|
+
if (displayName)
|
|
858
|
+
proposal.display_name = displayName;
|
|
859
|
+
const question = optionalText(row.question);
|
|
860
|
+
if (question)
|
|
861
|
+
proposal.question = question;
|
|
862
|
+
const why = optionalText(row.why);
|
|
863
|
+
if (why)
|
|
864
|
+
proposal.why = why;
|
|
830
865
|
const list = byPath.get(recordPath);
|
|
831
866
|
if (list)
|
|
832
867
|
list.push(proposal);
|
|
@@ -975,6 +1010,9 @@ async function loadGenericRecords(dirAbs, exclude, repoBaseUrl, branch) {
|
|
|
975
1010
|
return [];
|
|
976
1011
|
}
|
|
977
1012
|
const repoRelDir = toRepoRelative(dirAbs);
|
|
1013
|
+
// 区画のディレクトリ名(`records` の親)。**絶対パスから取る**——cwd 相対の repoRelDir は
|
|
1014
|
+
// cwd がリポジトリルートでないと `records` に倒れる(isMemberRecord の注意書き)
|
|
1015
|
+
const sectionDir = path.basename(dirAbs) === "records" ? path.basename(path.dirname(dirAbs)) : path.basename(dirAbs);
|
|
978
1016
|
const records = [];
|
|
979
1017
|
for (const e of entries) {
|
|
980
1018
|
if (!e.isFile() || !e.name.toLowerCase().endsWith(".md"))
|
|
@@ -982,7 +1020,7 @@ async function loadGenericRecords(dirAbs, exclude, repoBaseUrl, branch) {
|
|
|
982
1020
|
if (exclude.has(e.name.toLowerCase()))
|
|
983
1021
|
continue;
|
|
984
1022
|
const raw = await node_fs_1.promises.readFile(path.join(dirAbs, e.name), "utf8");
|
|
985
|
-
records.push(await parseGeneric(raw, e.name, repoRelDir, repoBaseUrl, branch));
|
|
1023
|
+
records.push(await parseGeneric(raw, e.name, repoRelDir, repoBaseUrl, branch, sectionDir));
|
|
986
1024
|
}
|
|
987
1025
|
// 日付降順 → タイトル昇順(用語集のように日付が同一でも安定して並ぶ)
|
|
988
1026
|
//
|
|
@@ -1000,7 +1038,7 @@ async function loadGenericRecords(dirAbs, exclude, repoBaseUrl, branch) {
|
|
|
1000
1038
|
});
|
|
1001
1039
|
return records;
|
|
1002
1040
|
}
|
|
1003
|
-
async function parseGeneric(raw, fileName, repoRelDir, repoBaseUrl, branch) {
|
|
1041
|
+
async function parseGeneric(raw, fileName, repoRelDir, repoBaseUrl, branch, sectionDir = "") {
|
|
1004
1042
|
const { data, content } = (0, gray_matter_1.default)(raw);
|
|
1005
1043
|
const h1 = content.match(/^#\s+(.+)$/m);
|
|
1006
1044
|
const id = String(data.id ?? fileName.replace(/\.md$/i, ""));
|
|
@@ -1028,7 +1066,7 @@ async function parseGeneric(raw, fileName, repoRelDir, repoBaseUrl, branch) {
|
|
|
1028
1066
|
bodyHtml,
|
|
1029
1067
|
bodyRef: "",
|
|
1030
1068
|
bodyId: "",
|
|
1031
|
-
searchText: toSearchText(content),
|
|
1069
|
+
searchText: memberSearchText(data, sectionDir, toSearchText(content)),
|
|
1032
1070
|
fileName,
|
|
1033
1071
|
editUrl,
|
|
1034
1072
|
rtype: data.type != null ? String(data.type) : "",
|
package/dist/external-id.js
CHANGED
|
@@ -27,6 +27,7 @@ exports.isExternalId = isExternalId;
|
|
|
27
27
|
exports.splitTypedId = splitTypedId;
|
|
28
28
|
exports.isMemberRecord = isMemberRecord;
|
|
29
29
|
exports.isMemberRecordPath = isMemberRecordPath;
|
|
30
|
+
exports.memberSearchTerms = memberSearchTerms;
|
|
30
31
|
/**
|
|
31
32
|
* このリポジトリの内側の呼び名。外の ID と見なさない。
|
|
32
33
|
*
|
|
@@ -128,7 +129,9 @@ function isMemberRecord(type, id, sectionDir) {
|
|
|
128
129
|
return true;
|
|
129
130
|
if (typeof id === "string" && id.startsWith("member:"))
|
|
130
131
|
return true;
|
|
131
|
-
|
|
132
|
+
// 日本語のディレクトリ名を使っている案件があるので、旧名「メンバー」も新名「関係者」も引く
|
|
133
|
+
//(画面の isMemberRec と同じ 3 つ・#704)
|
|
134
|
+
return sectionDir === "Members" || sectionDir === "メンバー" || sectionDir === "関係者";
|
|
132
135
|
}
|
|
133
136
|
/** そのリポジトリ相対パスが関係者の記録の置き場か(git のパスは cwd に依存しない) */
|
|
134
137
|
function isMemberRecordPath(repoPath) {
|
|
@@ -146,3 +149,36 @@ function isMemberRecordPath(repoPath) {
|
|
|
146
149
|
exports.TYPED_ID_WHOLE_SRC = "^" + TYPED_ID_SRC + "$";
|
|
147
150
|
/** 内側の呼び名の一覧(同上。埋め込み JS が `splitTypedId` と同じ除外を持つため) */
|
|
148
151
|
exports.INSIDE_KIND_LIST = [...INSIDE_KINDS];
|
|
152
|
+
/**
|
|
153
|
+
* 関係者の記録の検索索引に足す語(#704)。**ID の値で関係者を引けるようにする。**
|
|
154
|
+
*
|
|
155
|
+
* - `identities` の各行の `id` を**全体**(`slack_user:U0…`)で入れる。画面の検索は部分一致なので、
|
|
156
|
+
* 人が値だけ(`U0…`)を打っても当たる——種類を外した値を別に足すのは冗長なので足さない
|
|
157
|
+
* - `email` の欄(文字列で中身があるとき)
|
|
158
|
+
*
|
|
159
|
+
* 🔴 **`not_aliases`(否定済み)は入れない。** 「私ではない」と言った ID で引いて
|
|
160
|
+
* その人が出てくると、検索が「この ID はこの人だ」と言っているように読める。
|
|
161
|
+
* 🔴 **関係者以外の記録には呼ばない**(呼び出し側が isMemberRecord で絞る)。
|
|
162
|
+
*/
|
|
163
|
+
function memberSearchTerms(fm) {
|
|
164
|
+
const out = [];
|
|
165
|
+
const push = (v) => {
|
|
166
|
+
const t = v.trim();
|
|
167
|
+
if (t && !out.includes(t))
|
|
168
|
+
out.push(t);
|
|
169
|
+
};
|
|
170
|
+
const list = fm[exports.FM_LINKED_IDS];
|
|
171
|
+
if (Array.isArray(list)) {
|
|
172
|
+
for (const ent of list) {
|
|
173
|
+
if (!ent || typeof ent !== "object" || Array.isArray(ent))
|
|
174
|
+
continue;
|
|
175
|
+
const id = ent.id;
|
|
176
|
+
if (typeof id !== "string" || id.trim() === "")
|
|
177
|
+
continue;
|
|
178
|
+
push(id);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (typeof fm.email === "string")
|
|
182
|
+
push(fm.email);
|
|
183
|
+
return out;
|
|
184
|
+
}
|
package/dist/render.js
CHANGED
|
@@ -4562,6 +4562,7 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
|
|
|
4562
4562
|
.fm-rel { display: flex; gap: 6px; align-items: center; margin: 1px 0; }
|
|
4563
4563
|
/* つながっている ID の欄は1行1件(配列セルの join に落とすと [object Object] になる) */
|
|
4564
4564
|
.fm-li { margin: 1px 0; }
|
|
4565
|
+
.fm-empty { color: var(--muted); }
|
|
4565
4566
|
|
|
4566
4567
|
/* ---- explorer (Miller columns) ---- */
|
|
4567
4568
|
.mc-count { flex: 0 0 auto; font-size: 11px; color: var(--muted); background: #eceef1; border-radius: 999px; padding: 0 8px; }
|
|
@@ -4822,13 +4823,13 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
|
|
|
4822
4823
|
**本文の後に足す、機械が書いたことを人が確かめる節**で種類が同じ。
|
|
4823
4824
|
|
|
4824
4825
|
🔴 **表に <table> を使わない。** 画面の部品に table は1つも無く、table の CSS は
|
|
4825
|
-
本文(.body table)用しか無い。
|
|
4826
|
+
本文(.body table)用しか無い。5列(#704 で場を足した)のグリッドは .fm-grid と同じ書き方で組む。
|
|
4826
4827
|
🔴 各セルに min-width: 0 と overflow-wrap: anywhere を効かせる——ID は
|
|
4827
4828
|
切れ目の無い長い文字列になりうるので、入れないと詳細ページ全体が横に伸びる */
|
|
4828
4829
|
.linked-ids { margin-top: 28px; padding-top: 16px; border-top: 1px solid var(--border); }
|
|
4829
4830
|
.li-summary { cursor: pointer; font-size: 13.5px; font-weight: 700; color: var(--muted); }
|
|
4830
4831
|
.li-grid {
|
|
4831
|
-
display: grid; grid-template-columns:
|
|
4832
|
+
display: grid; grid-template-columns: 88px minmax(0, 1.5fr) minmax(0, 1fr) 88px minmax(0, 0.8fr); gap: 7px 14px;
|
|
4832
4833
|
font-size: 13px; margin-top: 10px;
|
|
4833
4834
|
background: rgba(255,255,255,0.55); border: 1px solid var(--border); border-radius: var(--radius);
|
|
4834
4835
|
padding: 14px 18px;
|
|
@@ -4842,6 +4843,12 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
|
|
|
4842
4843
|
.li-basis.is-firm { color: #15803d; }
|
|
4843
4844
|
.li-basis.is-machine { color: var(--muted); }
|
|
4844
4845
|
.li-basis.is-plain { color: var(--text); }
|
|
4846
|
+
/* 否定済み(not_aliases)の畳み。つながっている側より一段控えめに(#704) */
|
|
4847
|
+
.li-denied { margin-top: 10px; font-size: 13px; }
|
|
4848
|
+
.li-denied-summary { cursor: pointer; font-size: 12.5px; color: var(--muted); }
|
|
4849
|
+
.li-denied-list { margin: 6px 0 0; padding-left: 20px; }
|
|
4850
|
+
.li-denied-row { overflow-wrap: anywhere; color: var(--muted); }
|
|
4851
|
+
.li-denied-kind { display: inline-block; min-width: 72px; margin-right: 8px; font-weight: 700; }
|
|
4845
4852
|
/* 違っていたときにどうするかの断り。.ah-source と同じ置き方(節の末尾の小さな段落) */
|
|
4846
4853
|
.li-note { font-size: 12px; color: var(--muted); line-height: 1.7; margin: 12px 0 0; }
|
|
4847
4854
|
/* ---- レコードの削除(?op=delete-record)----
|
|
@@ -6500,7 +6507,20 @@ ${calendarAddUrl.toString()}
|
|
|
6500
6507
|
return hasProposals(rec) ? el("span", { class: "badge proposed", text: "提案あり" }) : null;
|
|
6501
6508
|
}
|
|
6502
6509
|
function hasProposals(rec) {
|
|
6503
|
-
return
|
|
6510
|
+
return openProposals(rec).length > 0;
|
|
6511
|
+
}
|
|
6512
|
+
/**
|
|
6513
|
+
* 画面に出す提案(**答え済みの名寄せの提案を除く**・#704)。
|
|
6514
|
+
*
|
|
6515
|
+
* 🔴 claim_key の ID が、記録の identities(同じ人と答えた)か not_aliases(別人と答えた)に
|
|
6516
|
+
* もう在るなら、そのカードは**出さない**——従来のカードにも落とさない。人が答えたあと、
|
|
6517
|
+
* engine がカードを取り下げるまでの夜(最長 14 日)に同じ問いを出し続けると、
|
|
6518
|
+
* [別人です]を押し直して宣言した行を黙って切る形になる。バッジ・絞り込み・「AIで適用」も
|
|
6519
|
+
* 同じ集合を見る(数と中身が食い違わないように)
|
|
6520
|
+
*/
|
|
6521
|
+
function openProposals(rec) {
|
|
6522
|
+
var ps = (rec && rec.proposals) || [];
|
|
6523
|
+
return ps.filter(function (one) { return !ipAnswered(rec, one); });
|
|
6504
6524
|
}
|
|
6505
6525
|
|
|
6506
6526
|
// 状態の絞り込み。**未確認と提案ありを同じセレクトに並べる**(導線を増やさない)
|
|
@@ -6810,7 +6830,7 @@ ${calendarAddUrl.toString()}
|
|
|
6810
6830
|
* 1 人が「名前の別表記の提案」と「ID の提案」を同時に持つときは、束ねる側は別表記の分だけ。
|
|
6811
6831
|
*/
|
|
6812
6832
|
function bundledProposals(rec, sectionName) {
|
|
6813
|
-
var ps = (rec
|
|
6833
|
+
var ps = openProposals(rec);
|
|
6814
6834
|
return ps.filter(function (one) { return !identityAsk(rec, sectionName, one); });
|
|
6815
6835
|
}
|
|
6816
6836
|
function aiApplyBtn(rec, sectionName, title) {
|
|
@@ -6863,20 +6883,21 @@ ${calendarAddUrl.toString()}
|
|
|
6863
6883
|
// 関係者の記録に外の ID を 1 つ足す提案は、差分の表ではなく**人への問い**で出す。
|
|
6864
6884
|
// 機械が決めきれなかったものを人が決める場なので、答えは 2 つだけ(同じ人/別人)。
|
|
6865
6885
|
//
|
|
6866
|
-
// 🔴 **門を 1 つでも欠いたら、今までどおりのカードに倒す。**
|
|
6867
|
-
//
|
|
6868
|
-
//
|
|
6869
|
-
//
|
|
6870
|
-
//
|
|
6886
|
+
// 🔴 **門を 1 つでも欠いたら、今までどおりのカードに倒す。** 新しい形にする道は 2 本:
|
|
6887
|
+
//
|
|
6888
|
+
// (A) engine の便 A(cortex-engine #540)の形——kind: "identity" と claim_key(足す ID そのもの)
|
|
6889
|
+
// を持つ提案(#704 で build の許可リストに足した)。門は
|
|
6890
|
+
// 1. 記録が関係者
|
|
6891
|
+
// 2. field が aliases(engine の現物)か identities(FM_LINKED_IDS)
|
|
6892
|
+
// 3. claim_key が種類つきの ID の形で、proposed の要素に在り、current の要素に無い
|
|
6893
|
+
// **カンマ連結の差集合から推測しない**ので、別名にカンマが入っても壊れない。
|
|
6894
|
+
// (B) それより前の形(kind も claim_key も無い)——
|
|
6895
|
+
// 1. 記録が関係者
|
|
6896
|
+
// 2. field が aliases
|
|
6897
|
+
// 3. proposed から current を引いた差に、種類つきの ID がちょうど 1 つ
|
|
6871
6898
|
//
|
|
6872
|
-
// 🔴 **kind
|
|
6873
|
-
//
|
|
6874
|
-
// 写していない**ので、実データでは one.kind が常に undefined——枝は本番で 1 度も
|
|
6875
|
-
// 通らない死に枝だった。この便の決定は「**提案に新しい欄を 1 つも通さない**」なので、
|
|
6876
|
-
// 許可リストに kind を足す側は採らない。
|
|
6877
|
-
// 🔴 **engine が kind: "identity" を出し始める便では、tools 側の「許可リスト
|
|
6878
|
-
//(readGoldProposals)」と「この門」の両方に足すこと。** 片方だけだと、engine が出しても
|
|
6879
|
-
// build が捨てる(届かない)か、届いても門が見ていない(使われない)。
|
|
6899
|
+
// 🔴 **kind が付いていても claim_key が無ければ (B) に落とす**(kind だけで新しい形にしない)。
|
|
6900
|
+
// 🔴 **kind は field の代わりにはならない**——field が別の項目(重複の疑い 等)なら今までどおり。
|
|
6880
6901
|
//
|
|
6881
6902
|
// 🔴 **見分けは種類つきの形(コロン付き)だけで行う。** 「@名前」「素のメール」は門に使わない
|
|
6882
6903
|
// ——aliases にはハンドル名が普通に入るので、@yamada が門を通ると、engine が
|
|
@@ -6954,6 +6975,56 @@ ${calendarAddUrl.toString()}
|
|
|
6954
6975
|
function ipMemberSection(name) {
|
|
6955
6976
|
return name === "Members" || name === "メンバー" || name === "関係者";
|
|
6956
6977
|
}
|
|
6978
|
+
/** カンマ連結の値を要素に割る(空白を落とす・空の断片は捨てる)。要素の有無を見るだけに使う */
|
|
6979
|
+
function ipLooseParts(v) {
|
|
6980
|
+
return (v == null ? "" : String(v)).split(",").map(function (x) { return x.trim(); })
|
|
6981
|
+
.filter(function (x) { return x !== ""; });
|
|
6982
|
+
}
|
|
6983
|
+
/**
|
|
6984
|
+
* 道 (A): engine が足す ID を claim_key で名指ししている提案(便 A・#704)。
|
|
6985
|
+
*
|
|
6986
|
+
* 🔴 **claim_key を鵜呑みにしない。** 種類つきの形で・内側の呼び名でなく・proposed に在り・
|
|
6987
|
+
* current に無いときだけ通す——すでに結び付いている ID について「同じ人ですか」と聞くと、
|
|
6988
|
+
* [別人です]で人が自分で宣言した行が not_aliases で黙って切れる(ipAdded と同じ理由)
|
|
6989
|
+
*/
|
|
6990
|
+
function ipAskByClaimKey(one) {
|
|
6991
|
+
if (one.field !== "aliases" && one.field !== FM_LINKED_IDS) return null;
|
|
6992
|
+
var typed = ipSplitTypedId(one.claim_key);
|
|
6993
|
+
if (!typed) return null;
|
|
6994
|
+
var whole = typed.kind + ":" + typed.value;
|
|
6995
|
+
var norm = function (x) { var t = ipSplitTypedId(x); return t ? t.kind + ":" + t.value : x; };
|
|
6996
|
+
if (ipLooseParts(one.proposed).map(norm).indexOf(whole) < 0) return null;
|
|
6997
|
+
if (ipLooseParts(one.current).map(norm).indexOf(whole) >= 0) return null;
|
|
6998
|
+
return typed;
|
|
6999
|
+
}
|
|
7000
|
+
/**
|
|
7001
|
+
* ID を突き合わせ用の綴りにそろえる(全角・コロンの前後の空白を落とす)。形に当たらなければ空文字。
|
|
7002
|
+
* engine の normalizeIdentityId(コロンの前後の空白を落とす)と同じ寄せ方
|
|
7003
|
+
*/
|
|
7004
|
+
function ipNormId(x) {
|
|
7005
|
+
if (typeof x !== "string") return "";
|
|
7006
|
+
var s = x.normalize("NFKC").replace(/:/g, ":").trim().replace(/^([^:]*?)\\s*:\\s*/, "$1:");
|
|
7007
|
+
var t = ipSplitTypedId(s);
|
|
7008
|
+
return t ? t.kind + ":" + t.value : "";
|
|
7009
|
+
}
|
|
7010
|
+
/**
|
|
7011
|
+
* 名寄せの提案が**答え済み**か(claim_key の ID が記録の identities[].id か not_aliases に在る)。
|
|
7012
|
+
* kind: identity と claim_key を持つ提案(engine の便 A の形)だけを見る
|
|
7013
|
+
*/
|
|
7014
|
+
function ipAnswered(rec, one) {
|
|
7015
|
+
if (!one || one.kind !== "identity") return false;
|
|
7016
|
+
var key = ipNormId(one.claim_key);
|
|
7017
|
+
if (!key) return false;
|
|
7018
|
+
var fm = (rec && rec.fm) || {};
|
|
7019
|
+
var denied = Array.isArray(fm.not_aliases) ? fm.not_aliases : [];
|
|
7020
|
+
for (var i = 0; i < denied.length; i++) if (ipNormId(denied[i]) === key) return true;
|
|
7021
|
+
var ids = Array.isArray(fm[FM_LINKED_IDS]) ? fm[FM_LINKED_IDS] : [];
|
|
7022
|
+
for (var j = 0; j < ids.length; j++) {
|
|
7023
|
+
var ent = ids[j];
|
|
7024
|
+
if (ent && typeof ent === "object" && !Array.isArray(ent) && ipNormId(ent.id) === key) return true;
|
|
7025
|
+
}
|
|
7026
|
+
return false;
|
|
7027
|
+
}
|
|
6957
7028
|
/**
|
|
6958
7029
|
* この提案を「同じ人ですか」の形にしてよいか。よければ増える ID の種類と値を返す。
|
|
6959
7030
|
* **3 つの門は常に AND**(上のコメントの 1・2・3)。
|
|
@@ -6961,6 +7032,9 @@ ${calendarAddUrl.toString()}
|
|
|
6961
7032
|
function identityAsk(rec, sectionName, one) {
|
|
6962
7033
|
if (!one) return null;
|
|
6963
7034
|
if (!isMemberRec(rec) && !ipMemberSection(sectionName)) return null;
|
|
7035
|
+
if (one.kind === "identity" && typeof one.claim_key === "string" && one.claim_key.trim() !== "") {
|
|
7036
|
+
return ipAskByClaimKey(one);
|
|
7037
|
+
}
|
|
6964
7038
|
if (one.field !== "aliases") return null;
|
|
6965
7039
|
var added = ipAdded(one.current, one.proposed);
|
|
6966
7040
|
if (added === null) return null;
|
|
@@ -6972,10 +7046,9 @@ ${calendarAddUrl.toString()}
|
|
|
6972
7046
|
return ids.length === 1 ? ids[0] : null;
|
|
6973
7047
|
}
|
|
6974
7048
|
/**
|
|
6975
|
-
*
|
|
7049
|
+
* 問いの文(engine が question を出していないときの組み立て)。
|
|
6976
7050
|
*
|
|
6977
|
-
* 🔴 **名乗り(display_name)は engine
|
|
6978
|
-
* 来た日に切り替えられるよう口だけ開けてある(いまの呼び出しは空文字を渡す)。
|
|
7051
|
+
* 🔴 **名乗り(display_name)は engine が出したときだけ使う。推測で名前を作らない。**
|
|
6979
7052
|
* 道具の読める名前は「つながっている ID」の対応表と**同じもの**を引く(2 度書かない)。
|
|
6980
7053
|
*/
|
|
6981
7054
|
function ipQuestion(toolLabel, value, displayName) {
|
|
@@ -6990,10 +7063,26 @@ ${calendarAddUrl.toString()}
|
|
|
6990
7063
|
// 🔴 **囲いの外に出る値も aiFenceScrub を通す**(パスは外部から書ける)
|
|
6991
7064
|
return aiFenceScrub((pathRef ? pathRef + " と " : "") + g.dir + "/README.md");
|
|
6992
7065
|
}
|
|
7066
|
+
/** 文字列で中身があるときだけ返す(engine の新しい欄は無いことがある) */
|
|
7067
|
+
function ipText(v) {
|
|
7068
|
+
return typeof v === "string" && v.trim() !== "" ? v.trim() : "";
|
|
7069
|
+
}
|
|
7070
|
+
/**
|
|
7071
|
+
* カードの見出しの問い。🔴 **engine の question を優先する**(便 A・#534 で「人への問い」と
|
|
7072
|
+
* 「機械の事情」を別の欄にした)。無い提案(便 A より前の形)は画面で組み立てる
|
|
7073
|
+
*/
|
|
7074
|
+
function ipAskText(one, typed) {
|
|
7075
|
+
return ipText(one.question) || ipQuestion(liKindLabel(typed.kind), typed.value, ipText(one.display_name));
|
|
7076
|
+
}
|
|
7077
|
+
/** 折りたたみに入れる機械の事情。why があればそれ、無ければ reason(前の形) */
|
|
7078
|
+
function ipWhyText(one) {
|
|
7079
|
+
return ipText(one.why) || ipText(one.reason);
|
|
7080
|
+
}
|
|
6993
7081
|
/** 囲いに入れる提案データの行(何を判定するのか・機械の事情・根拠) */
|
|
6994
7082
|
function ipDataLines(one, typed) {
|
|
6995
7083
|
var lines = ["判定の対象: " + typed.kind + ":" + typed.value];
|
|
6996
|
-
|
|
7084
|
+
var why = ipWhyText(one);
|
|
7085
|
+
if (why) lines.push("機械が決めきれなかった理由: " + why);
|
|
6997
7086
|
(one.evidence || []).forEach(function (e) {
|
|
6998
7087
|
lines.push("根拠: " + e.source + (e.quote ? " — 「" + e.quote + "」" : ""));
|
|
6999
7088
|
});
|
|
@@ -7013,13 +7102,13 @@ ${calendarAddUrl.toString()}
|
|
|
7013
7102
|
* claude.ai/code のディープリンクで、書くのは対話の向こう側。
|
|
7014
7103
|
*
|
|
7015
7104
|
* 🔴 **「1 行足す」と書き、配列ごと置換させない。** 🔴 **basis は書かせない**
|
|
7016
|
-
|
|
7105
|
+
*(書かないことが「本人が申告」の意味になる)。欄名の綴りは FM_LINKED_IDS 1 本から引く。
|
|
7017
7106
|
*/
|
|
7018
7107
|
function ipSameBtn(rec, sectionName, title, one, typed) {
|
|
7019
7108
|
var whole = typed.kind + ":" + typed.value;
|
|
7020
7109
|
var lines = [
|
|
7021
7110
|
ipTarget(rec, sectionName) + " を読んでから、この関係者「" + aiFenceScrub(title) + "」の " + FM_LINKED_IDS +
|
|
7022
|
-
" に - id: \\"" + whole + "\\" の**1 行だけ**足してください(**basis
|
|
7111
|
+
" に - id: \\"" + whole + "\\" の**1 行だけ**足してください(**basis は書かないでください**=「本人が申告」の意味になります)。",
|
|
7023
7112
|
"欄が無ければ作ってください。**配列ごと置き換えないでください。** aliases は触らないでください。",
|
|
7024
7113
|
"",
|
|
7025
7114
|
].concat(aiFenced(ipDataLines(one, typed))).concat([""]).concat(ipTail(rec));
|
|
@@ -7059,12 +7148,12 @@ ${calendarAddUrl.toString()}
|
|
|
7059
7148
|
/** 「同じ人ですか」のカード 1 枚。**ボタンはカードごと**(束ねる側からは外してある) */
|
|
7060
7149
|
function ipCard(rec, sectionName, one, typed) {
|
|
7061
7150
|
var item = el("div", { class: "proposal proposal-identity" }, [
|
|
7062
|
-
el("div", { class: "ip-ask", text:
|
|
7151
|
+
el("div", { class: "ip-ask", text: ipAskText(one, typed) }),
|
|
7063
7152
|
]);
|
|
7064
7153
|
var ev = proposalEvidenceList(one);
|
|
7065
7154
|
if (ev) item.appendChild(ev);
|
|
7066
|
-
// ID
|
|
7067
|
-
//
|
|
7155
|
+
// ID の値は小さく添える。リンクにしてよい形(許可リストで検査・Slack と Backlog は
|
|
7156
|
+
// 行き先のドメインが 1 つに決まるときだけ)は「つながっている ID」と同じ規則で決める
|
|
7068
7157
|
var idCell = el("div", { class: "ip-id" }, [el("span", { text: "ID: " })]);
|
|
7069
7158
|
var whole = typed.kind + ":" + typed.value;
|
|
7070
7159
|
var href = liHref(typed.kind, typed.value);
|
|
@@ -7076,10 +7165,11 @@ ${calendarAddUrl.toString()}
|
|
|
7076
7165
|
ipSameBtn(rec, sectionName, rec.title, one, typed),
|
|
7077
7166
|
ipDiffBtn(rec, sectionName, rec.title, one, typed),
|
|
7078
7167
|
]));
|
|
7079
|
-
|
|
7168
|
+
var why = ipWhyText(one);
|
|
7169
|
+
if (why) {
|
|
7080
7170
|
item.appendChild(el("details", { class: "ip-why" }, [
|
|
7081
7171
|
el("summary", { class: "ip-why-summary", text: "機械が決めきれなかった理由" }),
|
|
7082
|
-
el("div", { class: "ip-why-text", text:
|
|
7172
|
+
el("div", { class: "ip-why-text", text: why }),
|
|
7083
7173
|
]));
|
|
7084
7174
|
}
|
|
7085
7175
|
if (one.createdAt) {
|
|
@@ -7095,7 +7185,7 @@ ${calendarAddUrl.toString()}
|
|
|
7095
7185
|
* 任意文字列で、innerHTML に渡すと出典の引用がそのまま画面のコードになる。
|
|
7096
7186
|
*/
|
|
7097
7187
|
function proposalsSection(rec, sectionName) {
|
|
7098
|
-
var ps = (rec
|
|
7188
|
+
var ps = openProposals(rec);
|
|
7099
7189
|
if (!ps.length) return null;
|
|
7100
7190
|
var box = el("section", { class: "proposals" });
|
|
7101
7191
|
var head = el("div", { class: "proposals-head" }, [
|
|
@@ -17563,7 +17653,7 @@ ${calendarAddUrl.toString()}
|
|
|
17563
17653
|
if (meta.children.length) box.appendChild(meta);
|
|
17564
17654
|
return box;
|
|
17565
17655
|
}
|
|
17566
|
-
// ---------- 関係者詳細の「つながっている ID」(#686
|
|
17656
|
+
// ---------- 関係者詳細の「つながっている ID」(#686・#704 で値・場・リンク・否定済み) ----------
|
|
17567
17657
|
//
|
|
17568
17658
|
// 名寄せが「この人だ」と結んだ外の ID を、**どうやって分かったか**と一緒に並べる節。
|
|
17569
17659
|
// 人がここを見て違いに気づけることが目的なので、根拠を伏せない。
|
|
@@ -17577,26 +17667,30 @@ ${calendarAddUrl.toString()}
|
|
|
17577
17667
|
// 道具の読める名前。🔴 **語彙の外の種類は、その綴りをそのまま出す**
|
|
17578
17668
|
//(engine が新しい種類を足した晩に、道具の欄が空になって壊れないように)
|
|
17579
17669
|
var LI_KIND_LABEL = {
|
|
17580
|
-
email: "メール", git_author: "git", github_login: "GitHub", slack_user: "Slack",
|
|
17670
|
+
email: "メール", git_author: "git の作者", github_login: "GitHub", slack_user: "Slack",
|
|
17581
17671
|
google_chat_user: "Google Chat", figma_user: "Figma", backlog_user: "Backlog", portal_user: "ポータル",
|
|
17582
17672
|
};
|
|
17583
17673
|
// 根拠の日本語と色。🔴 **2つの語彙を受ける**——#534 の設計の語(verified / declared /
|
|
17584
|
-
// exact / name_match / judged)と、engine
|
|
17585
|
-
//
|
|
17674
|
+
// exact / name_match / judged)と、engine のコードに実在した語(name_only / probabilistic)。
|
|
17675
|
+
// どちらの綴りで書かれても同じ日本語に寄せる(片方しか受けないと、書いた晩から空欄になる)。
|
|
17676
|
+
// 🔴 **言葉はオーナーの決定(2026-09-23・#704)**: ポータルで確定/本人が申告/メール一致/
|
|
17677
|
+
// 名前一致/AI が判定。レコード情報のパネルも同じ表を引く(2 度書かない)
|
|
17586
17678
|
var LI_BASIS = {
|
|
17587
|
-
verified: { text: "
|
|
17588
|
-
declared: { text: "
|
|
17589
|
-
exact: { text: "
|
|
17590
|
-
name_match: { text: "
|
|
17591
|
-
name_only: { text: "
|
|
17592
|
-
judged: { text: "AI
|
|
17593
|
-
probabilistic: { text: "AI
|
|
17679
|
+
verified: { text: "ポータルで確定", tone: "is-firm" },
|
|
17680
|
+
declared: { text: "本人が申告", tone: "is-firm" },
|
|
17681
|
+
exact: { text: "メール一致", tone: "is-firm" },
|
|
17682
|
+
name_match: { text: "名前一致", tone: "is-machine" },
|
|
17683
|
+
name_only: { text: "名前一致", tone: "is-machine" },
|
|
17684
|
+
judged: { text: "AI が判定", tone: "is-machine" },
|
|
17685
|
+
probabilistic: { text: "AI が判定", tone: "is-machine" },
|
|
17594
17686
|
};
|
|
17595
17687
|
// 🔴 **文字列でも数でもない basis(配列・オブジェクト)の出し方。** そのまま String() すると
|
|
17596
17688
|
// 「[object Object]」が画面に出る。語彙の外は「そのまま出す」が規則だが、**そのまま出せない値**
|
|
17597
17689
|
// はこの 1 語に寄せる(色は中立のまま)。分類 B・決定ログ 20260920-686.md の B-19
|
|
17598
17690
|
var LI_BASIS_UNREADABLE = "(根拠を読み取れません)";
|
|
17599
|
-
|
|
17691
|
+
// 🔴 **5 列目の「場」(scope)は #704 で足した。** engine は綴りだけ予約していて、いまは
|
|
17692
|
+
// 人が手で書いたときだけ入る。無い行は素の空(「いつから」と同じ——空が正常)
|
|
17693
|
+
var LI_COLS = ["道具", "この人のページ", "どうやって分かったか", "いつから", "場"];
|
|
17600
17694
|
var LI_TYPED_ID = /^([A-Za-z][A-Za-z0-9_]*):(.+)$/;
|
|
17601
17695
|
// 🔴 **リンクにしてよい値の許可リスト。** 拒否リストにすると ? と & が通り、
|
|
17602
17696
|
// mailto:a@b?bcc=...&body=... の形のヘッダ注入が残る。
|
|
@@ -17607,7 +17701,21 @@ ${calendarAddUrl.toString()}
|
|
|
17607
17701
|
// a@b?bcc=... は当たってしまう——mailto: の URL で意味を持つ記号(クエリの始まり・
|
|
17608
17702
|
// 区切り・フラグメント・パーセント)をもう一段外して、ヘッダ注入の形を落とす
|
|
17609
17703
|
var LI_MAIL_META = /[?#&%]/;
|
|
17704
|
+
// Slack のユーザー ID(U… / W…・英大文字と数字だけ)と、ワークスペースのドメイン
|
|
17705
|
+
//(acme.slack.com の acme の部分。fleet-status.json の slackWorkspaces[].domain)
|
|
17706
|
+
var LI_SLACK_USER = /^[UW][A-Z0-9]{2,20}$/;
|
|
17707
|
+
var LI_SLACK_DOMAIN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
|
|
17708
|
+
var LI_SLACK_TEAM = /^[TE][A-Z0-9]{2,20}$/;
|
|
17709
|
+
// Backlog のユーザー(engine は userId=ログイン名、無い相手は数値 id を入れる)と、
|
|
17710
|
+
// スペースのドメイン(fleet の backlogTarget。正本は 課題管理/issues-config.json)。
|
|
17711
|
+
// 🔴 **数字だけの値はリンクにしない**——ユーザーページの URL はログイン名で組むので、
|
|
17712
|
+
// 数値 id で組むと開いた先に誰も居ない(または別人が出る)
|
|
17713
|
+
var LI_BACKLOG_USER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
17714
|
+
var LI_BACKLOG_DOMAIN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.backlog\\.(?:jp|com)$/i;
|
|
17610
17715
|
var LI_NOTE = "違うものがあれば「AIで編集」から「この ID は私ではない」と伝えると、翌晩から結ばなくなります。";
|
|
17716
|
+
// 否定済み(not_aliases)の畳みの見出し。🔴 **中は値だけでリンクにしない**——
|
|
17717
|
+
// 「私ではない」と言った先へ飛ばす口を出すと、つながっている側と見分けがつかなくなる
|
|
17718
|
+
var LI_DENIED_TITLE = "これは私ではない(否定済み)";
|
|
17611
17719
|
|
|
17612
17720
|
/** 種類 → 道具の読める名前。語彙の外はその綴りのまま */
|
|
17613
17721
|
function liKindLabel(kind) {
|
|
@@ -17616,7 +17724,7 @@ ${calendarAddUrl.toString()}
|
|
|
17616
17724
|
/**
|
|
17617
17725
|
* 根拠 → 画面の言葉と色。
|
|
17618
17726
|
*
|
|
17619
|
-
* 🔴 **basis
|
|
17727
|
+
* 🔴 **basis が無い行は「本人が申告」**(engine の決め。人がいちばん書きやすい
|
|
17620
17728
|
* id だけの 1 行が、いちばん強い宣言になる)。
|
|
17621
17729
|
* 🔴 **語彙の外はそのまま出して、中立の色に置く**——知らない根拠を、確かな側にも
|
|
17622
17730
|
* 機械の側にも倒さない
|
|
@@ -17625,7 +17733,7 @@ ${calendarAddUrl.toString()}
|
|
|
17625
17733
|
// 🔴 **declared(いちばん強い段)に倒してよいのは「欄が無い/空」のときだけ。**
|
|
17626
17734
|
// 以前は typeof raw === "string" でない値を一律に空扱いしていたので、
|
|
17627
17735
|
// basis: 123 や basis: {}(engine が形を崩した日・人が手で書き損ねた日)が
|
|
17628
|
-
//
|
|
17736
|
+
// **人の宣言**として確かな側の色で出ていた。読めない値を昇格させない
|
|
17629
17737
|
if (raw == null) return LI_BASIS.declared;
|
|
17630
17738
|
if (typeof raw !== "string") {
|
|
17631
17739
|
var shown = typeof raw === "number" || typeof raw === "boolean" || typeof raw === "bigint"
|
|
@@ -17639,22 +17747,84 @@ ${calendarAddUrl.toString()}
|
|
|
17639
17747
|
if (Object.prototype.hasOwnProperty.call(LI_BASIS, lower)) return LI_BASIS[lower];
|
|
17640
17748
|
return { text: key, tone: "is-plain" };
|
|
17641
17749
|
}
|
|
17642
|
-
/**
|
|
17643
|
-
* その行をリンクにしてよければ href を返す(だめなら空文字=値だけ出す)。
|
|
17644
|
-
*
|
|
17645
|
-
* 🔴 Slack・Backlog・Figma・Google Chat・その他は**値だけ**。人のページの URL の形が
|
|
17646
|
-
* どこにも書かれていないので、ホスト名から推測して組まない。
|
|
17647
|
-
*/
|
|
17648
17750
|
/** メールの形に当たり、mailto: の URL を壊す記号を含まないときだけ href を返す */
|
|
17649
17751
|
function liMailto(value) {
|
|
17650
17752
|
return LI_EMAIL.test(value) && !LI_MAIL_META.test(value) ? "mailto:" + value : "";
|
|
17651
17753
|
}
|
|
17652
|
-
|
|
17754
|
+
/**
|
|
17755
|
+
* Slack のユーザーのページを組むワークスペースのドメイン(決められなければ空文字)。
|
|
17756
|
+
*
|
|
17757
|
+
* 🔴 **既定の決め方(分類 B・決定ログ 20260923-704.md)**:
|
|
17758
|
+
* 1. 行の場(scope)が接続済みワークスペースの team_id(T…)と一致すれば、その
|
|
17759
|
+
* ワークスペース。一致した先にドメインが無ければ**リンク無し**(他へ倒さない)。
|
|
17760
|
+
* 場が team_id の形なのにどれとも一致しなければ**リンク無し**
|
|
17761
|
+
* 2. そうでなく、接続済みワークスペースが**ちょうど 1 つ**ならそれ
|
|
17762
|
+
* 3. それ以外(0 個・2 個以上)は**リンク無し**
|
|
17763
|
+
* 🔴 **チャンネルの URL のホスト名から推測しない**(改名で変わるし、共有チャンネルでは
|
|
17764
|
+
* 貼った人のホストになる。Slack の節と同じ作法)。**間違ったリンクは値だけより悪い**
|
|
17765
|
+
*/
|
|
17766
|
+
function liSlackDomain(scope) {
|
|
17767
|
+
var list = slackWorkspaces() || [];
|
|
17768
|
+
var pick = null;
|
|
17769
|
+
var want = typeof scope === "string" ? scope.trim() : "";
|
|
17770
|
+
if (want !== "") {
|
|
17771
|
+
for (var i = 0; i < list.length; i++) {
|
|
17772
|
+
if (list[i] && list[i].teamId === want) { pick = list[i]; break; }
|
|
17773
|
+
}
|
|
17774
|
+
// 🔴 **場が team_id の形(T… / E…)なのに接続済みのどれとも一致しないなら、リンク無し。**
|
|
17775
|
+
// 別のワークスペースの人を、接続済みのワークスペースのページへ飛ばさない
|
|
17776
|
+
if (!pick && LI_SLACK_TEAM.test(want)) return "";
|
|
17777
|
+
}
|
|
17778
|
+
if (!pick && list.length === 1) pick = list[0];
|
|
17779
|
+
var d = pick && typeof pick.domain === "string" ? pick.domain.trim() : "";
|
|
17780
|
+
return LI_SLACK_DOMAIN.test(d) ? d : "";
|
|
17781
|
+
}
|
|
17782
|
+
/**
|
|
17783
|
+
* Backlog のユーザーのページを組むスペースのドメイン(決められなければ空文字)。
|
|
17784
|
+
*
|
|
17785
|
+
* 🔴 **登録簿(issues-config.json)はここで読まない**(ビルドも画面も読まない約束・
|
|
17786
|
+
* no-registry-read)。同じ値を engine が写した fleet の backlogTarget を、接続先の一覧と
|
|
17787
|
+
* 同じ関数(backlogTargetsNow)で引く。
|
|
17788
|
+
* 🔴 **スペースが 1 つに決まるときだけ**(接続先が 2 つあっても同じスペースなら 1 つ)。
|
|
17789
|
+
* 行の場(scope)がどれかのスペースのドメインと一致すればそれを採り、スペースの形なのに
|
|
17790
|
+
* どれとも一致しなければリンク無し
|
|
17791
|
+
*/
|
|
17792
|
+
function liBacklogDomain(scope) {
|
|
17793
|
+
var seen = [];
|
|
17794
|
+
backlogTargetsNow(null).forEach(function (t) {
|
|
17795
|
+
if (!t || t.pending) return;
|
|
17796
|
+
var d = String(t.domain || "").trim().toLowerCase();
|
|
17797
|
+
if (d && seen.indexOf(d) < 0) seen.push(d);
|
|
17798
|
+
});
|
|
17799
|
+
var want = typeof scope === "string" ? scope.trim().toLowerCase() : "";
|
|
17800
|
+
// 🔴 **場が Backlog のスペースの形なのに接続先のどれとも一致しないなら、リンク無し。**
|
|
17801
|
+
// 別のスペースの人を、接続済みのスペースのページへ飛ばさない(Slack の team_id と同じ守り)
|
|
17802
|
+
if (want && seen.indexOf(want) < 0 && LI_BACKLOG_DOMAIN.test(want)) return "";
|
|
17803
|
+
var d = want && seen.indexOf(want) >= 0 ? want : seen.length === 1 ? seen[0] : "";
|
|
17804
|
+
return LI_BACKLOG_DOMAIN.test(d) ? d : "";
|
|
17805
|
+
}
|
|
17806
|
+
/**
|
|
17807
|
+
* その行をリンクにしてよければ href を返す(だめなら空文字=値だけ出す)。
|
|
17808
|
+
*
|
|
17809
|
+
* 🔴 **値も行き先のドメインも、両方を許可リストで検査してから組む。** Figma・Google Chat・
|
|
17810
|
+
* その他は値だけ(人のページの URL の形がどこにも書かれていない)
|
|
17811
|
+
*/
|
|
17812
|
+
function liHref(kind, value, scope) {
|
|
17653
17813
|
if (kind === "github_login") return LI_GITHUB_LOGIN.test(value) ? "https://github.com/" + value : "";
|
|
17654
17814
|
if (kind === "email") return liMailto(value);
|
|
17655
17815
|
// 🔴 git_author の値はメールとは限らない(engine は authorEmail か authorName を入れる)。
|
|
17656
17816
|
// アットマークを含み、かつメールの形に当たるときだけリンクにする
|
|
17657
17817
|
if (kind === "git_author") return value.indexOf("@") >= 0 ? liMailto(value) : "";
|
|
17818
|
+
if (kind === "slack_user") {
|
|
17819
|
+
if (!LI_SLACK_USER.test(value)) return "";
|
|
17820
|
+
var sd = liSlackDomain(scope);
|
|
17821
|
+
return sd ? "https://" + sd + ".slack.com/team/" + value : "";
|
|
17822
|
+
}
|
|
17823
|
+
if (kind === "backlog_user") {
|
|
17824
|
+
if (!LI_BACKLOG_USER.test(value) || /^[0-9]+$/.test(value)) return "";
|
|
17825
|
+
var bd = liBacklogDomain(scope);
|
|
17826
|
+
return bd ? "https://" + bd + "/user/" + value : "";
|
|
17827
|
+
}
|
|
17658
17828
|
return "";
|
|
17659
17829
|
}
|
|
17660
17830
|
/** 関係者の記録か(画面側は型と区画名で判る。判定の正本は src/external-id.ts の isMemberRecord) */
|
|
@@ -17672,7 +17842,6 @@ ${calendarAddUrl.toString()}
|
|
|
17672
17842
|
* 配列でない・要素がオブジェクトでない・id が文字列でない行は捨てる。
|
|
17673
17843
|
* 🔴 **並べ替えない**(engine は人の行を人が書いた順に、機械の行を id の辞書順に書く。
|
|
17674
17844
|
* 画面で並べ直すと、記録と画面の行が対応しなくなる)。
|
|
17675
|
-
* 🔴 scope は読まない(綴りだけ予約されている欄)。
|
|
17676
17845
|
*/
|
|
17677
17846
|
function linkedIdRows(rec) {
|
|
17678
17847
|
var fm = rec && rec.fm;
|
|
@@ -17688,47 +17857,89 @@ ${calendarAddUrl.toString()}
|
|
|
17688
17857
|
var kind = typed ? typed[1] : "";
|
|
17689
17858
|
var value = typed ? typed[2] : whole;
|
|
17690
17859
|
var basis = liBasis(ent.basis);
|
|
17860
|
+
// 場は文字列のときだけ(数やオブジェクトを String() して出さない)
|
|
17861
|
+
var scope = typeof ent.scope === "string" ? ent.scope.trim() : "";
|
|
17691
17862
|
out.push({
|
|
17692
17863
|
kindLabel: liKindLabel(kind),
|
|
17693
17864
|
value: value,
|
|
17694
|
-
href: liHref(kind, value),
|
|
17865
|
+
href: liHref(kind, value, scope),
|
|
17695
17866
|
basisText: basis.text,
|
|
17696
17867
|
tone: basis.tone,
|
|
17697
17868
|
// 🔴 since が無い行の「いつから」は素の空。印(ダッシュ等)を足さない——空が正常
|
|
17698
17869
|
since: typeof ent.since === "string" ? ent.since : "",
|
|
17870
|
+
scope: scope,
|
|
17699
17871
|
});
|
|
17700
17872
|
});
|
|
17701
17873
|
return out;
|
|
17702
17874
|
}
|
|
17703
17875
|
/**
|
|
17704
|
-
*
|
|
17705
|
-
|
|
17876
|
+
* 否定済み(not_aliases)の行。**文字列の要素だけ**を、書かれた順のまま返す。
|
|
17877
|
+
* <種類>:<値> の形なら道具の名前と値に割り、素の文字列はそのまま値として出す
|
|
17878
|
+
*/
|
|
17879
|
+
function deniedIdRows(rec) {
|
|
17880
|
+
var list = rec && rec.fm ? rec.fm.not_aliases : null;
|
|
17881
|
+
if (!Array.isArray(list)) return [];
|
|
17882
|
+
var out = [];
|
|
17883
|
+
list.forEach(function (one) {
|
|
17884
|
+
if (typeof one !== "string") return;
|
|
17885
|
+
var whole = one.trim();
|
|
17886
|
+
if (whole === "") return;
|
|
17887
|
+
var typed = LI_TYPED_ID.exec(whole);
|
|
17888
|
+
out.push({ kindLabel: typed ? liKindLabel(typed[1]) : "", value: typed ? typed[2] : whole });
|
|
17889
|
+
});
|
|
17890
|
+
return out;
|
|
17891
|
+
}
|
|
17892
|
+
/**
|
|
17893
|
+
* 「つながっている ID」の折りたたみ。**関係者以外の記録と、つながっている ID も
|
|
17894
|
+
* 否定済みも 0 件のときは null**(節ごと出さない=欄が無い記録は今までと同じに見える)。
|
|
17706
17895
|
*
|
|
17707
17896
|
* 🔴 **表に table を使わない**(画面の部品に table は1つも無い)。.fm-grid と同じ
|
|
17708
|
-
*
|
|
17897
|
+
* 書き方のグリッドで組む。値は全部 el() の text(textContent 経由)で入れる。
|
|
17898
|
+
* 🔴 **否定済みは同じ節の中に、さらに畳んで出す**(開かなければ見えない)。
|
|
17899
|
+
* つながっている ID が無く否定済みだけある記録にも節を出す——「前に否定したか」を
|
|
17900
|
+
* 確かめる口が画面から消えないように
|
|
17709
17901
|
*/
|
|
17710
17902
|
function buildLinkedIds(rec) {
|
|
17711
17903
|
if (!isMemberRec(rec)) return null;
|
|
17712
17904
|
var rows = linkedIdRows(rec);
|
|
17713
|
-
|
|
17905
|
+
var denied = deniedIdRows(rec);
|
|
17906
|
+
if (!rows.length && !denied.length) return null;
|
|
17907
|
+
var head = "つながっている ID(" + rows.length + "件" + (denied.length ? "・否定済み " + denied.length + "件" : "") + ")";
|
|
17714
17908
|
var det = el("details", { class: "linked-ids" }, [
|
|
17715
|
-
el("summary", { class: "li-summary", text:
|
|
17909
|
+
el("summary", { class: "li-summary", text: head }),
|
|
17716
17910
|
]);
|
|
17717
|
-
|
|
17718
|
-
|
|
17719
|
-
|
|
17720
|
-
|
|
17721
|
-
|
|
17722
|
-
|
|
17723
|
-
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
|
|
17728
|
-
|
|
17729
|
-
|
|
17730
|
-
|
|
17731
|
-
|
|
17911
|
+
if (rows.length) {
|
|
17912
|
+
var grid = el("div", { class: "li-grid" });
|
|
17913
|
+
LI_COLS.forEach(function (col) {
|
|
17914
|
+
grid.appendChild(el("div", { class: "li-cell li-head", text: col }));
|
|
17915
|
+
});
|
|
17916
|
+
rows.forEach(function (row) {
|
|
17917
|
+
grid.appendChild(el("div", { class: "li-cell li-kind", text: row.kindLabel }));
|
|
17918
|
+
var valueCell = el("div", { class: "li-cell li-value" });
|
|
17919
|
+
valueCell.appendChild(row.href
|
|
17920
|
+
? el("a", { href: row.href, target: "_blank", rel: "noopener", text: row.value })
|
|
17921
|
+
: el("span", { text: row.value }));
|
|
17922
|
+
grid.appendChild(valueCell);
|
|
17923
|
+
grid.appendChild(el("div", { class: "li-cell li-basis " + row.tone, text: row.basisText }));
|
|
17924
|
+
grid.appendChild(el("div", { class: "li-cell li-since", text: row.since }));
|
|
17925
|
+
grid.appendChild(el("div", { class: "li-cell li-scope", text: row.scope }));
|
|
17926
|
+
});
|
|
17927
|
+
det.appendChild(grid);
|
|
17928
|
+
}
|
|
17929
|
+
if (denied.length) {
|
|
17930
|
+
var box = el("details", { class: "li-denied" }, [
|
|
17931
|
+
el("summary", { class: "li-denied-summary", text: LI_DENIED_TITLE + "(" + denied.length + "件)" }),
|
|
17932
|
+
]);
|
|
17933
|
+
var ul = el("ul", { class: "li-denied-list" });
|
|
17934
|
+
denied.forEach(function (d) {
|
|
17935
|
+
var li = el("li", { class: "li-denied-row" });
|
|
17936
|
+
if (d.kindLabel) li.appendChild(el("span", { class: "li-denied-kind", text: d.kindLabel }));
|
|
17937
|
+
li.appendChild(el("span", { class: "li-denied-value", text: d.value }));
|
|
17938
|
+
ul.appendChild(li);
|
|
17939
|
+
});
|
|
17940
|
+
box.appendChild(ul);
|
|
17941
|
+
det.appendChild(box);
|
|
17942
|
+
}
|
|
17732
17943
|
// 違っていたときにどうするか。.ah-source と同じ置き方(節の末尾の小さな段落)
|
|
17733
17944
|
det.appendChild(el("p", { class: "li-note", text: LI_NOTE }));
|
|
17734
17945
|
return det;
|
|
@@ -19164,6 +19375,14 @@ ${calendarAddUrl.toString()}
|
|
|
19164
19375
|
if (isHttp(target)) return el("a", { href: target, target: "_blank", rel: "noopener", text: target });
|
|
19165
19376
|
return el("span", { text: target });
|
|
19166
19377
|
}
|
|
19378
|
+
var FM_EMPTY_TEXT = "(未記入)";
|
|
19379
|
+
/** パネルの 1 値を文字にする。オブジェクト・配列は JSON([object Object] を出さない) */
|
|
19380
|
+
function fmScalarText(x) {
|
|
19381
|
+
if (x && typeof x === "object") {
|
|
19382
|
+
try { return JSON.stringify(x); } catch (e) { return String(x); }
|
|
19383
|
+
}
|
|
19384
|
+
return String(x);
|
|
19385
|
+
}
|
|
19167
19386
|
function renderFmPanel(fm) {
|
|
19168
19387
|
if (!fm || !Object.keys(fm).length) return null;
|
|
19169
19388
|
var panel = el("details", { class: "fm-panel" });
|
|
@@ -19173,7 +19392,12 @@ ${calendarAddUrl.toString()}
|
|
|
19173
19392
|
grid.appendChild(el("div", { class: "fm-key", text: k }));
|
|
19174
19393
|
var cell = el("div", { class: "fm-val" });
|
|
19175
19394
|
var v = fm[k];
|
|
19176
|
-
if (
|
|
19395
|
+
if (v === "" || (Array.isArray(v) && v.length === 0)) {
|
|
19396
|
+
// 🔴 **空を空欄で表さない**(#704)。記録に email: "" と書かれている(雛形の既定)
|
|
19397
|
+
// だけなのに、画面が値を伏せているように見えた——「書かれていない」と言葉で出す。
|
|
19398
|
+
// identities: [] / relations: [] もここで受ける(下の欄ごとの枝より前で見る)
|
|
19399
|
+
cell.appendChild(el("span", { class: "fm-empty", text: FM_EMPTY_TEXT }));
|
|
19400
|
+
} else if (k === "relations" && Array.isArray(v)) {
|
|
19177
19401
|
v.forEach(function (r) {
|
|
19178
19402
|
if (!r || typeof r !== "object") return;
|
|
19179
19403
|
var row = el("div", { class: "fm-rel" });
|
|
@@ -19198,7 +19422,10 @@ ${calendarAddUrl.toString()}
|
|
|
19198
19422
|
cell.appendChild(line);
|
|
19199
19423
|
});
|
|
19200
19424
|
} else if (Array.isArray(v)) {
|
|
19201
|
-
|
|
19425
|
+
// 🔴 要素がオブジェクトなら JSON で出す(join に落とすと [object Object] になる)
|
|
19426
|
+
cell.textContent = v.map(fmScalarText).join(" / ");
|
|
19427
|
+
} else if (v && typeof v === "object") {
|
|
19428
|
+
cell.textContent = fmScalarText(v);
|
|
19202
19429
|
} else if (k === "source") {
|
|
19203
19430
|
cell.appendChild(fmTargetLink(String(v)));
|
|
19204
19431
|
} else if (isHttp(v)) {
|
package/package.json
CHANGED