@unson/brainbase-mcp 0.41.7 → 0.41.8
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/graph-foundation-reader.d.ts +6 -2
- package/dist/graph-foundation-reader.js +10 -9
- package/package.json +2 -2
- package/ui/foundation-overview.css +1 -8
- package/ui/foundation-overview.js +98 -20
- package/ui/graph-own-share.js +28 -9
- package/ui/outcome-knowledge.css +18 -0
- package/ui/outcome-knowledge.js +265 -9
- package/ui/world/world-work-rail.js +15 -2
|
@@ -78,9 +78,13 @@ export interface FoundationObjectiveDetails {
|
|
|
78
78
|
readonly evaluator_note?: unknown;
|
|
79
79
|
readonly current_state?: unknown;
|
|
80
80
|
}
|
|
81
|
-
/**
|
|
81
|
+
/** The words a World Model record gives for where it applies. */
|
|
82
|
+
export interface FoundationModelDetails {
|
|
83
|
+
readonly applicability_text?: unknown;
|
|
84
|
+
}
|
|
85
|
+
/** Runtime shape used by the overview contract for Objective and World Model records. */
|
|
82
86
|
export interface FoundationOverviewObjectiveRecord extends FoundationCatalogRecord {
|
|
83
|
-
readonly details?: FoundationObjectiveDetails;
|
|
87
|
+
readonly details?: FoundationObjectiveDetails | FoundationModelDetails;
|
|
84
88
|
}
|
|
85
89
|
export interface GraphFoundationReaders {
|
|
86
90
|
readonly store: Pick<FoundationRevisionStore, 'read' | 'readLatest' | 'list'>;
|
|
@@ -525,12 +525,10 @@ function validateFoundationRecord(row, reference, context, selectedProjectCode,
|
|
|
525
525
|
definition: cloneFoundationDefinition(history.definition),
|
|
526
526
|
digest: digestFoundationDefinition(history.definition)
|
|
527
527
|
};
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
return record;
|
|
528
|
+
const details = reference.type === 'objective'
|
|
529
|
+
? projectDetails(history.payload, OBJECTIVE_DETAIL_KEYS)
|
|
530
|
+
: reference.type === 'model' ? projectDetails(history.payload, MODEL_DETAIL_KEYS) : undefined;
|
|
531
|
+
return details === undefined ? record : { ...record, details };
|
|
534
532
|
}
|
|
535
533
|
const OBJECTIVE_DETAIL_KEYS = [
|
|
536
534
|
'title',
|
|
@@ -540,15 +538,18 @@ const OBJECTIVE_DETAIL_KEYS = [
|
|
|
540
538
|
'evaluator_note',
|
|
541
539
|
'current_state'
|
|
542
540
|
];
|
|
543
|
-
|
|
541
|
+
// A model's foundation scope carries ids and a registration time; its own words for where it
|
|
542
|
+
// applies live beside the definition.
|
|
543
|
+
const MODEL_DETAIL_KEYS = ['applicability_text'];
|
|
544
|
+
function projectDetails(payload, keys) {
|
|
544
545
|
const nestedDetails = isPlainRecord(payload.details) ? payload.details : undefined;
|
|
545
546
|
const foundation = isPlainRecord(payload.foundation) ? payload.foundation : undefined;
|
|
546
547
|
// Graphify payloads historically carried these descriptive fields at the
|
|
547
548
|
// payload root. Keep the two known nested forms compatible while exposing
|
|
548
|
-
// only the
|
|
549
|
+
// only the listed fields in the overview contract.
|
|
549
550
|
const candidates = [payload, nestedDetails, foundation].filter((candidate) => candidate !== undefined);
|
|
550
551
|
const projected = {};
|
|
551
|
-
for (const key of
|
|
552
|
+
for (const key of keys) {
|
|
552
553
|
const source = candidates.find((candidate) => Object.prototype.hasOwnProperty.call(candidate, key));
|
|
553
554
|
if (source !== undefined && source[key] !== undefined)
|
|
554
555
|
projected[key] = source[key];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.8",
|
|
4
4
|
"description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -483,5 +483,5 @@
|
|
|
483
483
|
"engines": {
|
|
484
484
|
"node": ">=20"
|
|
485
485
|
},
|
|
486
|
-
"gitHead": "
|
|
486
|
+
"gitHead": "b061e1b0a7ccb22155c6e22459ad735dc5e7adb1"
|
|
487
487
|
}
|
|
@@ -151,14 +151,6 @@
|
|
|
151
151
|
line-height: 1.55;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
.bb-fov-record-id {
|
|
155
|
-
display: block;
|
|
156
|
-
margin-top: 7px;
|
|
157
|
-
color: var(--bb-color-quiet);
|
|
158
|
-
font-family: var(--bb-font-mono);
|
|
159
|
-
font-size: 10px;
|
|
160
|
-
overflow-wrap: anywhere;
|
|
161
|
-
}
|
|
162
154
|
|
|
163
155
|
.bb-fov-ref-list {
|
|
164
156
|
display: grid;
|
|
@@ -254,4 +246,5 @@
|
|
|
254
246
|
@media (max-width: 760px) { .bb-fov-objective-ledger { overflow-x: auto; } .bb-fov-objective-ledger .bb-ws-ledger-row { min-width: 760px; } }
|
|
255
247
|
@media (max-width: 480px) { .bb-fov-reading .bb-ws-definition { grid-template-columns: 1fr; gap: 4px; } .bb-fov-reading dd { margin-bottom: 12px; } }
|
|
256
248
|
.bb-fov-philosophy-card .bb-fov-original { border-top: 0; padding-top: 0; }
|
|
249
|
+
.bb-fov-model-card .bb-fov-original { margin-top: 12px; padding-top: 10px; overflow-wrap: anywhere; }
|
|
257
250
|
.bb-fov-objective-ledger .bb-ws-ledger-row:focus-visible { outline: 2px solid var(--bb-color-focus); outline-offset: 2px; }
|
|
@@ -55,6 +55,16 @@ const PHILOSOPHY_STATUS_LABELS = Object.freeze({
|
|
|
55
55
|
archived: '終了',
|
|
56
56
|
});
|
|
57
57
|
const OPERATOR_LABELS = Object.freeze({ at_least: '以上', at_most: '以下', equals: '一致' });
|
|
58
|
+
const SOURCE_KIND_LABELS = Object.freeze({
|
|
59
|
+
candidate: '候補からの登録',
|
|
60
|
+
document: '文書',
|
|
61
|
+
observation: '観察',
|
|
62
|
+
decision: '決定',
|
|
63
|
+
import: '取り込み',
|
|
64
|
+
});
|
|
65
|
+
// A record without a name is said to have none; its id stays in the record details (P9).
|
|
66
|
+
const UNNAMED_RECORD = '名前の記録なし';
|
|
67
|
+
const UNNAMED_VARIABLE = '名前の記録がない変数';
|
|
58
68
|
|
|
59
69
|
export class FoundationOverviewError extends Error {
|
|
60
70
|
constructor(code, message) {
|
|
@@ -225,12 +235,52 @@ const SCOPE_LABELS = Object.freeze({
|
|
|
225
235
|
company: '組織',
|
|
226
236
|
});
|
|
227
237
|
|
|
238
|
+
function formatValidity(from, until) {
|
|
239
|
+
if (!from && !until) return null;
|
|
240
|
+
if (!from) return `${formatDate(until)} まで`;
|
|
241
|
+
return `${formatDate(from)} から ${until ? `${formatDate(until)} まで` : '(終了日の記録なし)'}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* A scope in words: `{ type, id }` names the project or organization, and a validity scope
|
|
246
|
+
* (`subjectIds`, `validFrom`, `validUntil`) becomes its period and the number of subjects. The
|
|
247
|
+
* subject ids themselves belong to the record details.
|
|
248
|
+
*/
|
|
228
249
|
function formatScope(value) {
|
|
229
250
|
if (!isObject(value)) return formatStructuredValue(value);
|
|
230
251
|
const type = formatAny(value.type);
|
|
231
252
|
const id = formatAny(value.id);
|
|
232
253
|
if (type && id) return `${SCOPE_LABELS[type] ?? type}: ${id}`;
|
|
233
|
-
|
|
254
|
+
const subjects = firstValue(value, 'subjectIds', 'subject_ids');
|
|
255
|
+
const period = formatValidity(firstValue(value, 'validFrom', 'valid_from'), firstValue(value, 'validUntil', 'valid_until'));
|
|
256
|
+
const parts = [
|
|
257
|
+
Array.isArray(subjects) && subjects.length > 0 ? `対象 ${subjects.length}件` : null,
|
|
258
|
+
period ? `適用期間: ${period}` : null,
|
|
259
|
+
].filter(Boolean);
|
|
260
|
+
if (parts.length > 0) return parts.join(' / ');
|
|
261
|
+
return formatAny(firstValue(value, 'text', 'description', 'statement', 'label', 'name'))
|
|
262
|
+
?? '読める形の記録がありません(記録の詳細を参照)';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Where a definition came from, as kinds of source; the source and evidence ids stay in the details. */
|
|
266
|
+
function formatProvenance(provenance) {
|
|
267
|
+
if (!Array.isArray(provenance) || provenance.length === 0) return null;
|
|
268
|
+
return provenance.map((item) => {
|
|
269
|
+
const kind = SOURCE_KIND_LABELS[item?.sourceKind] ?? '種類の記録なし';
|
|
270
|
+
const evidence = Array.isArray(item?.evidenceIds) ? item.evidenceIds.length : 0;
|
|
271
|
+
return evidence > 0 ? `${kind}(証拠${evidence}件)` : kind;
|
|
272
|
+
}).join('、');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Ids, revisions and digests of one record, for the "記録の詳細" disclosure only. */
|
|
276
|
+
function recordDetailsElement(doc, rows, summary = '記録の詳細') {
|
|
277
|
+
const details = makeElement(doc, 'details', { className: 'bb-fov-original' });
|
|
278
|
+
details.append(makeElement(doc, 'summary', { text: summary }), workspaceDefinition(doc, rows));
|
|
279
|
+
return details;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function referenceLabels(refs) {
|
|
283
|
+
return Array.isArray(refs) && refs.length > 0 ? refs.map(referenceLabel).join('、') : null;
|
|
234
284
|
}
|
|
235
285
|
|
|
236
286
|
function recordDefinition(record) { return isObject(record?.definition) ? record.definition : {}; }
|
|
@@ -265,6 +315,15 @@ function statusElement(doc, definition, className = '') {
|
|
|
265
315
|
return makeElement(doc, 'p', { className: `bb-fov-status${className ? ` ${className}` : ''}`, text: statusText(definition) });
|
|
266
316
|
}
|
|
267
317
|
|
|
318
|
+
/**
|
|
319
|
+
* The one state a reader needs in front of a model: whether it is held as verified or as a
|
|
320
|
+
* hypothesis. Adoption, validation and authorized uses stay in the record details.
|
|
321
|
+
*/
|
|
322
|
+
function certaintyText(definition) {
|
|
323
|
+
const label = EPISTEMIC_LABELS[definition.epistemicState] ?? VALIDATION_LABELS[definition.validationState];
|
|
324
|
+
return `確からしさ: ${label ?? '記録なし'}`;
|
|
325
|
+
}
|
|
326
|
+
|
|
268
327
|
function detailsFor(record) {
|
|
269
328
|
return isObject(record?.details) ? record.details : {};
|
|
270
329
|
}
|
|
@@ -310,9 +369,7 @@ function criteriaList(doc, definition, variableMap) {
|
|
|
310
369
|
const ref = criterion?.variableRef;
|
|
311
370
|
const exact = variableMap.get(foundationKey(ref));
|
|
312
371
|
const exactDefinition = exact ? recordDefinition(exact) : null;
|
|
313
|
-
const name = exactDefinition
|
|
314
|
-
? (formatAny(exactDefinition.meaning) ?? referenceLabel(ref))
|
|
315
|
-
: referenceLabel(ref);
|
|
372
|
+
const name = (exactDefinition ? formatAny(exactDefinition.meaning) : null) ?? UNNAMED_VARIABLE;
|
|
316
373
|
const variableDetails = exactDefinition
|
|
317
374
|
? [
|
|
318
375
|
['定義', exactDefinition.definition],
|
|
@@ -328,7 +385,7 @@ function criteriaList(doc, definition, variableMap) {
|
|
|
328
385
|
const status = exact ? '参照確認済み' : '参照未確認(基準変数の版が一致しません)';
|
|
329
386
|
list.append(makeElement(doc, 'li', {
|
|
330
387
|
className: exact ? '' : 'is-unrecorded',
|
|
331
|
-
text: `${name}${variableDetails.length > 0 ? `(${variableDetails.join(' / ')})` : ''}
|
|
388
|
+
text: `${name}${variableDetails.length > 0 ? `(${variableDetails.join(' / ')})` : ''} ${operator}${target === null ? '' : ` ${target}`} — ${status}`,
|
|
332
389
|
}));
|
|
333
390
|
}
|
|
334
391
|
return list;
|
|
@@ -395,7 +452,7 @@ function recordedStateElement(doc, record) {
|
|
|
395
452
|
function objectiveCard(doc, record, variableMap) {
|
|
396
453
|
const definition = recordDefinition(record);
|
|
397
454
|
const details = detailsFor(record);
|
|
398
|
-
const title = formatAny(firstValue(details, 'title')) ?? formatAny(definition.meaning) ??
|
|
455
|
+
const title = formatAny(firstValue(details, 'title')) ?? formatAny(definition.meaning) ?? UNNAMED_RECORD;
|
|
399
456
|
const gaps = missingObjectiveFields(record);
|
|
400
457
|
const current = currentStateFor(record);
|
|
401
458
|
const evaluationPeriod = formatAny(firstValue(details, 'evaluation_period_note', 'evaluationPeriodNote'))
|
|
@@ -446,11 +503,16 @@ function objectiveReading(doc, record, variableMap, recordContexts) {
|
|
|
446
503
|
const provenance = Array.isArray(definition.provenance) ? definition.provenance : [];
|
|
447
504
|
section('出典', workspaceDefinition(doc, [
|
|
448
505
|
['記録元', recordContexts?.get(record)?.label ?? formatAny(record.source) ?? '未記録'],
|
|
449
|
-
['根拠',
|
|
506
|
+
['根拠', formatProvenance(provenance) ?? '未記録'],
|
|
450
507
|
]));
|
|
508
|
+
const criteriaRefs = Array.isArray(definition.criteria) ? definition.criteria.map((criterion) => criterion?.variableRef) : [];
|
|
451
509
|
const original = makeElement(doc, 'details', { className: 'bb-fov-original' });
|
|
452
510
|
original.append(makeElement(doc, 'summary', { text: '記録の詳細・参照・原文' }), objectiveCard(doc, record, variableMap),
|
|
453
|
-
workspaceDefinition(doc, [
|
|
511
|
+
workspaceDefinition(doc, [
|
|
512
|
+
['目的ID', definition.id], ['版', definition.revision], ['digest', record.digest],
|
|
513
|
+
['基準変数の参照', referenceLabels(criteriaRefs)],
|
|
514
|
+
['根拠の記録', provenance.length ? formatStructuredValue(provenance) : null],
|
|
515
|
+
]));
|
|
454
516
|
body.append(original); return body;
|
|
455
517
|
}
|
|
456
518
|
|
|
@@ -477,12 +539,12 @@ function renderObjectives(doc, catalog, viewState, callbacks, recordContexts) {
|
|
|
477
539
|
const opener = doc.activeElement;
|
|
478
540
|
viewState.dialog?.close();
|
|
479
541
|
viewState.dialog = workspaceDialog(doc, {
|
|
480
|
-
title: formatAny(details.title) ?? formatAny(definition.meaning) ??
|
|
542
|
+
title: formatAny(details.title) ?? formatAny(definition.meaning) ?? UNNAMED_RECORD, opener,
|
|
481
543
|
content: objectiveReading(doc, record, recordContexts?.get(record)?.variableMap ?? variableMap, recordContexts),
|
|
482
544
|
});
|
|
483
545
|
},
|
|
484
546
|
cells: [
|
|
485
|
-
{ primary: formatAny(details.title) ?? formatAny(definition.meaning) ??
|
|
547
|
+
{ primary: formatAny(details.title) ?? formatAny(definition.meaning) ?? UNNAMED_RECORD },
|
|
486
548
|
current.text ?? current.status ?? '未記録',
|
|
487
549
|
{ text: current.missing.length ? current.missing.join('・') : missingObjectiveFields(record).join('・') || '記録なし', className: 'bb-fov-missing' },
|
|
488
550
|
formatDate(current.asOf),
|
|
@@ -506,13 +568,13 @@ function philosophyApplicability(payload) {
|
|
|
506
568
|
const period = validFrom
|
|
507
569
|
? `${formatDate(validFrom)}から${validUntil ? ` ${formatDate(validUntil)}まで` : ''}`
|
|
508
570
|
: null;
|
|
509
|
-
return [scope, period ? `適用期間: ${period}` : null].filter(Boolean).join(' / ') ||
|
|
571
|
+
return [scope, period ? `適用期間: ${period}` : null].filter(Boolean).join(' / ') || formatScope(legacy);
|
|
510
572
|
}
|
|
511
573
|
|
|
512
574
|
function philosophyCard(doc, record) {
|
|
513
575
|
const payload = record.payload;
|
|
514
576
|
const card = makeElement(doc, 'article', { className: 'bb-fov-philosophy-card' });
|
|
515
|
-
const title = formatAny(payload.title) ??
|
|
577
|
+
const title = formatAny(payload.title) ?? UNNAMED_RECORD;
|
|
516
578
|
card.append(makeElement(doc, 'h3', { text: title }));
|
|
517
579
|
|
|
518
580
|
if (formatAny(payload.statement)) card.append(makeElement(doc, 'p', { className: 'bb-fov-philosophy-statement', text: formatAny(payload.statement) }));
|
|
@@ -569,10 +631,10 @@ function renderReferenceList(doc, refs, variableMap, label) {
|
|
|
569
631
|
}
|
|
570
632
|
for (const ref of refs) {
|
|
571
633
|
const exact = variableMap.get(foundationKey(ref));
|
|
572
|
-
const name = exact ? formatAny(recordDefinition(exact).meaning)
|
|
634
|
+
const name = (exact ? formatAny(recordDefinition(exact).meaning) : null) ?? UNNAMED_VARIABLE;
|
|
573
635
|
list.append(makeElement(doc, 'li', {
|
|
574
636
|
className: exact ? '' : 'is-unrecorded',
|
|
575
|
-
text: exact ?
|
|
637
|
+
text: exact ? name : `${name} — 未接続(変数の版が一致しません)`,
|
|
576
638
|
}));
|
|
577
639
|
}
|
|
578
640
|
return list;
|
|
@@ -581,16 +643,32 @@ function renderReferenceList(doc, refs, variableMap, label) {
|
|
|
581
643
|
function modelCard(doc, record, variableMap) {
|
|
582
644
|
const definition = recordDefinition(record);
|
|
583
645
|
const card = makeElement(doc, 'article', { className: 'bb-fov-model-card' });
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
646
|
+
// The stored scope is ids and a registration time, not the reader's "where it applies"; the
|
|
647
|
+
// record's own words go in front, and the stored scope waits in the details.
|
|
648
|
+
const applicability = definition.applicability ?? definition.scope;
|
|
649
|
+
const registered = isObject(applicability) ? applicability : {};
|
|
650
|
+
const subjects = firstValue(registered, 'subjectIds', 'subject_ids');
|
|
651
|
+
const applicabilityText = formatAny(firstValue(detailsFor(record), 'applicability_text', 'applicabilityText'))
|
|
652
|
+
?? (isObject(applicability) ? null : formatAny(applicability));
|
|
653
|
+
card.append(makeElement(doc, 'h3', { text: formatAny(definition.meaning) ?? UNNAMED_RECORD }));
|
|
654
|
+
card.append(makeElement(doc, 'p', { className: 'bb-fov-status', text: certaintyText(definition) }));
|
|
587
655
|
card.append(workspaceDefinition(doc, [
|
|
588
656
|
['関係', formatAny(definition.relationship)],
|
|
589
|
-
['適用範囲',
|
|
657
|
+
['適用範囲', applicabilityText ?? { text: '範囲の説明は記録されていません', className: 'is-unrecorded' }],
|
|
590
658
|
['不確かさ', formatAny(definition.uncertainty)],
|
|
591
659
|
['入力変数', renderReferenceList(doc, definition.inputVariableRefs, variableMap, '入力変数')],
|
|
592
660
|
['出力変数', renderReferenceList(doc, definition.outputVariableRefs, variableMap, '出力変数')],
|
|
593
661
|
]));
|
|
662
|
+
card.append(recordDetailsElement(doc, [
|
|
663
|
+
['状態の内訳', statusText(definition)],
|
|
664
|
+
['記録ID', definition.id],
|
|
665
|
+
['版', definition.revision],
|
|
666
|
+
['digest', record.digest],
|
|
667
|
+
['登録上の対象', Array.isArray(subjects) && subjects.length > 0 ? subjects.join('、') : null],
|
|
668
|
+
['登録上の開始日時', formatValidity(firstValue(registered, 'validFrom', 'valid_from'), firstValue(registered, 'validUntil', 'valid_until'))],
|
|
669
|
+
['入力変数の参照', referenceLabels(definition.inputVariableRefs)],
|
|
670
|
+
['出力変数の参照', referenceLabels(definition.outputVariableRefs)],
|
|
671
|
+
]));
|
|
594
672
|
return card;
|
|
595
673
|
}
|
|
596
674
|
|
|
@@ -712,7 +790,7 @@ export function renderFoundationOverview(root, state, {
|
|
|
712
790
|
surface.append(workspacePageHeader(doc, {
|
|
713
791
|
crumbs: ['Brainbase', '目的と現状'],
|
|
714
792
|
title: '目的と現状',
|
|
715
|
-
lead: '
|
|
793
|
+
lead: '選択したプロジェクトの目的、哲学、世界モデルを、記録された内容と未確認の点を分けて読みます。',
|
|
716
794
|
source: `正本: ${scopeId}`,
|
|
717
795
|
}));
|
|
718
796
|
}
|
|
@@ -734,7 +812,7 @@ export function renderFoundationOverview(root, state, {
|
|
|
734
812
|
return workspaceDisclosure(doc, { title, help, content, open: viewState[key] ?? initialOpen, onToggle: open => { viewState[key] = open; } });
|
|
735
813
|
};
|
|
736
814
|
surface.append(fold('哲学', '判断の前提となる考え方です。登録された本文と、判断への適用範囲を分けて表示します。', renderPhilosophies(doc, catalog, recordContexts), true));
|
|
737
|
-
surface.append(fold('世界モデル', '
|
|
815
|
+
surface.append(fold('世界モデル', '何をすると結果が変わるか、という仮説。検証済みか仮説かを先に示し、ほかの状態や記録の番号は各モデルの「記録の詳細」で確認できます。', renderModels(doc, catalog, viewState, callbacks, recordContexts), false));
|
|
738
816
|
root.append(surface);
|
|
739
817
|
renderLoadingState(doc, root, viewState, false, onLoading);
|
|
740
818
|
return surface;
|
package/ui/graph-own-share.js
CHANGED
|
@@ -76,12 +76,25 @@ export function graphOwnShareSource(option, { fetcher } = {}) {
|
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** What the owner sent:
|
|
79
|
+
/** What the owner sent, in words: when it was sent. The snapshot ID and digest are in ownShareHandoverRows. */
|
|
80
80
|
export function ownShareHandoverText(handover) {
|
|
81
81
|
if (!isRecord(handover) || !textOrNull(handover.graphId)) return null;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
return `${formatDateTime(handover.createdAt)} に手元のOSSから送った束`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The snapshot ID, where it is kept and its digest, for the record details only. */
|
|
86
|
+
export function ownShareHandoverRows(handover) {
|
|
87
|
+
if (!isRecord(handover) || !textOrNull(handover.graphId)) return null;
|
|
88
|
+
return [
|
|
89
|
+
['束ID', handover.graphId],
|
|
90
|
+
['置き場所', textOrNull(handover.projectCode)],
|
|
91
|
+
['digest', textOrNull(handover.digest) ?? '未確認'],
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function handoverDetails(doc, handover) {
|
|
96
|
+
const rows = ownShareHandoverRows(handover);
|
|
97
|
+
return rows ? recordDetails(doc, rows) : null;
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
/**
|
|
@@ -113,6 +126,8 @@ export function ownShareSection(doc, { label, lead = OWN_LEAD, handover, childre
|
|
|
113
126
|
const sent = ownShareHandoverText(handover);
|
|
114
127
|
if (sent) facts.append(makeElement(doc, 'span', { text: sent }));
|
|
115
128
|
section.append(facts);
|
|
129
|
+
const sentDetails = handoverDetails(doc, handover);
|
|
130
|
+
if (sentDetails) section.append(sentDetails);
|
|
116
131
|
for (const child of children) if (child) section.append(child);
|
|
117
132
|
return section;
|
|
118
133
|
}
|
|
@@ -250,7 +265,7 @@ function personText(person) {
|
|
|
250
265
|
}
|
|
251
266
|
|
|
252
267
|
function criterionText(criterion) {
|
|
253
|
-
const variable = textOrNull(criterion.variable?.meaning) ??
|
|
268
|
+
const variable = textOrNull(criterion.variable?.meaning) ?? '名前の記録がない変数';
|
|
254
269
|
if (criterion.target === null || criterion.target === undefined) return `${variable}(目標値なし)`;
|
|
255
270
|
const value = `${String(criterion.target)}${textOrNull(criterion.variable?.unit) ? ` ${criterion.variable.unit}` : ''}`;
|
|
256
271
|
if (criterion.operator === 'at_least') return `${variable}: ${value}以上`;
|
|
@@ -295,7 +310,7 @@ export function createOwnObjectivesView({
|
|
|
295
310
|
function detail(objective) {
|
|
296
311
|
const wrap = makeElement(doc, 'div', { className: 'bb-graph-own-detail', attrs: { 'aria-label': '選んだ目的(引き継いだ自分の分)' } });
|
|
297
312
|
wrap.append(
|
|
298
|
-
workspaceRailHead(doc, { kicker: '目的(引き継いだ自分の分)', title: objective.meaning
|
|
313
|
+
workspaceRailHead(doc, { kicker: '目的(引き継いだ自分の分)', title: objective.meaning }),
|
|
299
314
|
closeButton(doc, () => controller.select(null)),
|
|
300
315
|
);
|
|
301
316
|
const criteria = makeElement(doc, 'ul', { className: 'bb-graph-plain-list' });
|
|
@@ -312,7 +327,11 @@ export function createOwnObjectivesView({
|
|
|
312
327
|
['状態', ownLabel(ADOPTION_LABELS, objective.adoptionState) ?? textOrNull(objective.adoptionState)],
|
|
313
328
|
['公開範囲(手元での設定)', ownLabel(VISIBILITY_LABELS, objective.visibility) ?? textOrNull(objective.visibility)],
|
|
314
329
|
]),
|
|
315
|
-
recordDetails(doc, [
|
|
330
|
+
recordDetails(doc, [
|
|
331
|
+
['目的ID', objective.id],
|
|
332
|
+
['版', objective.revision],
|
|
333
|
+
['基準変数のID', objective.criteria.map((criterion) => textOrNull(criterion.variable?.id)).filter(Boolean).join('、') || null],
|
|
334
|
+
]),
|
|
316
335
|
],
|
|
317
336
|
}));
|
|
318
337
|
return wrap;
|
|
@@ -344,7 +363,7 @@ export function createOwnObjectivesView({
|
|
|
344
363
|
selected: objective.id === state.selectedId,
|
|
345
364
|
onSelect: (id) => controller.select(id),
|
|
346
365
|
cells: [
|
|
347
|
-
{ primary: objective.meaning
|
|
366
|
+
{ primary: objective.meaning },
|
|
348
367
|
ownLabel(ADOPTION_LABELS, objective.adoptionState) ?? String(objective.adoptionState ?? ''),
|
|
349
368
|
periodText(objective.evaluationPeriod),
|
|
350
369
|
personText(objective.accountable) ?? { text: '未登録', className: 'is-unresolved' },
|
|
@@ -365,7 +384,7 @@ export function createOwnObjectivesView({
|
|
|
365
384
|
if (compact) {
|
|
366
385
|
const facts = makeElement(doc, 'p', { className: 'bb-graph-own-facts', text: '読み取りのみ' });
|
|
367
386
|
const refs = makeElement(doc, 'details');
|
|
368
|
-
refs.append(makeElement(doc, 'summary', { text: '出典' }), makeElement(doc, 'p', { text: ownShareHandoverText(handover) ?? (state.list.state === 'not_handed_over' ? 'まだ引き継いでいません。' : '出典は確認できません。') }));
|
|
387
|
+
refs.append(makeElement(doc, 'summary', { text: '出典' }), makeElement(doc, 'p', { text: ownShareHandoverText(handover) ?? (state.list.state === 'not_handed_over' ? 'まだ引き継いでいません。' : '出典は確認できません。') }), handoverDetails(doc, handover));
|
|
369
388
|
root.replaceChildren(facts, ...body(), refs);
|
|
370
389
|
}
|
|
371
390
|
else root.replaceChildren(ownShareSection(doc, { label: source.label, lead: OBJECTIVES_LEAD, handover, children: body(), className: 'bb-graph-own-objectives' }));
|
package/ui/outcome-knowledge.css
CHANGED
|
@@ -287,6 +287,7 @@
|
|
|
287
287
|
-webkit-line-clamp: 2;
|
|
288
288
|
}
|
|
289
289
|
.knowledge-outcome .knowledge-row-meta { display: flex; flex-wrap: wrap; gap: .15rem .55rem; min-width: 0; }
|
|
290
|
+
.knowledge-outcome .knowledge-row-project { color: var(--knowledge-ink); font-size: .68rem; font-weight: 600; overflow-wrap: anywhere; }
|
|
290
291
|
.knowledge-outcome .knowledge-row-type,
|
|
291
292
|
.knowledge-outcome .knowledge-row-scope,
|
|
292
293
|
.knowledge-outcome .knowledge-row-state { color: var(--knowledge-muted); font-size: .68rem; white-space: nowrap; }
|
|
@@ -451,3 +452,20 @@
|
|
|
451
452
|
.knowledge-outcome .knowledge-skeleton-line { animation: none; }
|
|
452
453
|
.knowledge-outcome .knowledge-button { transition: none; }
|
|
453
454
|
}
|
|
455
|
+
|
|
456
|
+
.knowledge-outcome .knowledge-project-filter {
|
|
457
|
+
display: grid;
|
|
458
|
+
gap: .25rem;
|
|
459
|
+
min-width: min(16rem, 100%);
|
|
460
|
+
color: var(--knowledge-muted);
|
|
461
|
+
font-size: .72rem;
|
|
462
|
+
}
|
|
463
|
+
.knowledge-outcome .knowledge-list-truncated { margin-top: .25rem; color: var(--knowledge-muted); font-size: .72rem; }
|
|
464
|
+
.knowledge-outcome .knowledge-overview-owner {
|
|
465
|
+
display: grid;
|
|
466
|
+
gap: .5rem;
|
|
467
|
+
margin-top: .75rem;
|
|
468
|
+
border-top: 1px solid var(--knowledge-line);
|
|
469
|
+
padding-top: .75rem;
|
|
470
|
+
}
|
|
471
|
+
.knowledge-outcome .knowledge-overview-project { color: var(--knowledge-muted); font-size: .78rem; }
|
package/ui/outcome-knowledge.js
CHANGED
|
@@ -380,6 +380,7 @@ export function normalizeKnowledgeItem(item) {
|
|
|
380
380
|
return {
|
|
381
381
|
id: fieldValue(item, 'id', 'knowledge_id', 'canonical_id') ?? null,
|
|
382
382
|
type: fieldValue(item, 'type', 'kind', 'entity_type') ?? null,
|
|
383
|
+
project_code: fieldValue(item, 'project_code', 'projectCode') ?? null,
|
|
383
384
|
title: fieldValue(item, 'title', 'name', 'subject') ?? null,
|
|
384
385
|
summary: fieldValue(item, 'summary', 'statement', 'body') ?? null,
|
|
385
386
|
content: fieldValue(item, 'content', 'canonical_content') ?? null,
|
|
@@ -411,12 +412,31 @@ export function normalizeKnowledgeCollection(payload) {
|
|
|
411
412
|
records,
|
|
412
413
|
absence_confirmed: absenceConfirmed,
|
|
413
414
|
searched_scope: Array.isArray(body.searched_scope) ? body.searched_scope : null,
|
|
415
|
+
truncated: body.truncated === true,
|
|
414
416
|
query: fieldValue(body, 'query', 'q') ?? null,
|
|
415
417
|
warnings: Array.isArray(body.warnings) ? body.warnings : null,
|
|
416
418
|
raw: payload,
|
|
417
419
|
};
|
|
418
420
|
}
|
|
419
421
|
|
|
422
|
+
const HEADING_MAX_LENGTH = 80;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The heading shown for a knowledge record. A record without a title uses
|
|
426
|
+
* the first line of its own summary or body instead of a generic label, so
|
|
427
|
+
* the list still tells records apart. Nothing is invented: when none of the
|
|
428
|
+
* three exists the heading says the title is unconfirmed.
|
|
429
|
+
*/
|
|
430
|
+
export function knowledgeItemHeading(item) {
|
|
431
|
+
const title = optionalText(item?.title).trim();
|
|
432
|
+
if (title) return title;
|
|
433
|
+
for (const value of [item?.summary, item?.content]) {
|
|
434
|
+
const firstLine = optionalText(value).split(/\r?\n/u).map((line) => line.trim()).find(Boolean);
|
|
435
|
+
if (firstLine) return firstLine.length > HEADING_MAX_LENGTH ? `${firstLine.slice(0, HEADING_MAX_LENGTH)}…` : firstLine;
|
|
436
|
+
}
|
|
437
|
+
return 'タイトル未確認';
|
|
438
|
+
}
|
|
439
|
+
|
|
420
440
|
function isRecord(value) {
|
|
421
441
|
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
422
442
|
}
|
|
@@ -755,7 +775,8 @@ export function renderDiscoveryFilters(root, filters = {}, callbacks = {}) {
|
|
|
755
775
|
queryLabel.append(query);
|
|
756
776
|
const scopeFieldLabel = makeElement('label', { text: '範囲' });
|
|
757
777
|
const scope = makeElement('select', { className: 'knowledge-control', attrs: { name: 'scope' } });
|
|
758
|
-
|
|
778
|
+
const scopeOptionLabel = (value) => optionalText(callbacks.scopeLabels?.[value]).trim() || scopeLabel(value);
|
|
779
|
+
for (const value of ['project', 'organization', 'all']) append(scope, makeElement('option', { text: scopeOptionLabel(value), value, attrs: { value, ...(filters.scope === value ? { selected: 'selected' } : {}) } }));
|
|
759
780
|
scope.value = filters.scope || 'all';
|
|
760
781
|
scopeFieldLabel.append(scope);
|
|
761
782
|
const statusLabel = makeElement('label', { text: '状態' });
|
|
@@ -781,6 +802,18 @@ export function renderDiscoveryFilters(root, filters = {}, callbacks = {}) {
|
|
|
781
802
|
return form;
|
|
782
803
|
}
|
|
783
804
|
|
|
805
|
+
/**
|
|
806
|
+
* Where a record belongs, for lists that span several projects. Organization
|
|
807
|
+
* wide records say so instead of naming the project row that stores them.
|
|
808
|
+
*/
|
|
809
|
+
export function knowledgeItemProjectLabel(item, projectLabel) {
|
|
810
|
+
if (optionalText(item?.scope).trim().toLowerCase() === 'organization') return '組織共通';
|
|
811
|
+
const code = optionalText(item?.project_code).trim();
|
|
812
|
+
if (!code) return 'プロジェクト未確認';
|
|
813
|
+
const label = typeof projectLabel === 'function' ? optionalText(projectLabel(code)).trim() : '';
|
|
814
|
+
return label || code;
|
|
815
|
+
}
|
|
816
|
+
|
|
784
817
|
export function renderKnowledgeList(root, model = {}, callbacks = {}) {
|
|
785
818
|
clear(root);
|
|
786
819
|
const collection = model.records === undefined && (model.items || model.results || model.state) ? normalizeKnowledgeCollection(model) : model;
|
|
@@ -792,15 +825,24 @@ export function renderKnowledgeList(root, model = {}, callbacks = {}) {
|
|
|
792
825
|
&& ['ready', 'results', EMPTY].includes(state);
|
|
793
826
|
const section = makeElement('section', { className: 'knowledge-list-panel', attrs: { 'aria-labelledby': 'knowledge-list-title' } });
|
|
794
827
|
const header = makeElement('div', { className: 'knowledge-panel-head' });
|
|
795
|
-
|
|
796
|
-
|
|
828
|
+
const countText = countKnown
|
|
829
|
+
? `${collection.records.length}件${collection.truncated ? '(新しい順の上限まで)' : ''}`
|
|
830
|
+
: UNKNOWN;
|
|
831
|
+
const scopeText = typeof callbacks.scopeText === 'function'
|
|
832
|
+
? callbacks.scopeText(collection)
|
|
833
|
+
: searchedScope ? `検索範囲: ${searchedScope}` : '検索範囲: 未確認';
|
|
834
|
+
append(header, makeElement('div', {},), makeElement('span', { className: 'knowledge-count', text: countText }));
|
|
835
|
+
header.firstChild.append(makeElement('h3', { text: '知識・判断', attrs: { id: 'knowledge-list-title' } }), makeElement('p', { text: scopeText }));
|
|
836
|
+
if (collection.truncated && countKnown) {
|
|
837
|
+
header.firstChild.append(makeElement('p', { className: 'knowledge-list-truncated', text: '表示は新しい順の上限までです。キーワードやプロジェクトで絞ると、ほかの記録も見られます。' }));
|
|
838
|
+
}
|
|
797
839
|
section.append(header);
|
|
798
840
|
if (state === 'loading') {
|
|
799
841
|
const skeleton = makeElement('div', { className: 'knowledge-list-rows', attrs: { 'aria-label': '知識を読み込み中' } });
|
|
800
842
|
for (let index = 0; index < 4; index += 1) skeleton.append(skeletonRow());
|
|
801
843
|
section.append(skeleton);
|
|
802
844
|
} else if (state === EMPTY) {
|
|
803
|
-
section.append(makeElement('div', { className: 'knowledge-empty', text: 'この条件に一致する知識はありません。自然文から登録案を作成できます。' }));
|
|
845
|
+
section.append(makeElement('div', { className: 'knowledge-empty', text: callbacks.emptyText ?? 'この条件に一致する知識はありません。自然文から登録案を作成できます。' }));
|
|
804
846
|
} else if (state === 'permission_denied') {
|
|
805
847
|
// Keep the upstream reason and recovery action visible. Replacing the
|
|
806
848
|
// payload with a generic detail makes a denied scope look like an
|
|
@@ -818,13 +860,14 @@ export function renderKnowledgeList(root, model = {}, callbacks = {}) {
|
|
|
818
860
|
const hasSelectedId = callbacks.selectedId !== undefined && callbacks.selectedId !== null;
|
|
819
861
|
if (!collection.records.length) rows.append(makeElement('div', { className: 'knowledge-empty', text: '検索結果を確認できません。空の一覧とは扱いません。' }));
|
|
820
862
|
for (const item of collection.records) {
|
|
821
|
-
const title =
|
|
863
|
+
const title = knowledgeItemHeading(item);
|
|
822
864
|
const row = makeElement('button', { className: 'knowledge-list-row', attrs: { type: 'button', 'aria-pressed': String(item.id === callbacks.selectedId), 'aria-controls': 'knowledge-selected-detail', title } });
|
|
823
865
|
const main = makeElement('span', { className: 'knowledge-row-main' });
|
|
824
866
|
const meta = makeElement('span', { className: 'knowledge-row-meta' });
|
|
867
|
+
if (callbacks.showProject) meta.append(makeElement('span', { className: 'knowledge-row-project', text: knowledgeItemProjectLabel(item, callbacks.projectLabel) }));
|
|
825
868
|
append(meta,
|
|
826
869
|
makeElement('span', { className: 'knowledge-row-type', text: item.type ? typeLabel(item.type) : UNKNOWN }),
|
|
827
|
-
makeElement('span', { className: 'knowledge-row-scope', text: item.scope ? scopeLabel(item.scope) : UNKNOWN }),
|
|
870
|
+
callbacks.showProject ? null : makeElement('span', { className: 'knowledge-row-scope', text: item.scope ? scopeLabel(item.scope) : UNKNOWN }),
|
|
828
871
|
makeElement('span', { className: 'knowledge-row-state', text: item.lifecycle.status !== UNKNOWN ? lifecycleStatusLabel(item.lifecycle.status) : UNKNOWN }),
|
|
829
872
|
);
|
|
830
873
|
append(main,
|
|
@@ -895,14 +938,14 @@ export function renderKnowledgeDetail(root, item, callbacks = {}) {
|
|
|
895
938
|
}
|
|
896
939
|
append(section,
|
|
897
940
|
makeElement('span', { className: 'knowledge-eyebrow', text: item.type ? typeLabel(item.type) : UNKNOWN }),
|
|
898
|
-
makeElement('h3', { text:
|
|
941
|
+
makeElement('h3', { text: knowledgeItemHeading(item), attrs: { id: 'knowledge-detail-title' } }),
|
|
899
942
|
);
|
|
900
943
|
const summary = makeElement('p', { className: 'knowledge-long-text', text: item.summary ? text(item.summary) : '要旨未確認' });
|
|
901
944
|
section.append(summary);
|
|
902
945
|
renderDefinitionList(section, [
|
|
903
946
|
['正本ID', item.id ? text(item.id) : UNKNOWN],
|
|
904
947
|
['版', item.version !== null && item.version !== undefined && String(item.version).trim() !== '' ? String(item.version) : UNKNOWN],
|
|
905
|
-
['範囲', item.scope ? scopeLabel(item.scope) : UNKNOWN],
|
|
948
|
+
['範囲', item.scope ? (optionalText(callbacks.scopeLabels?.[optionalText(item.scope).trim().toLowerCase()]).trim() || scopeLabel(item.scope)) : UNKNOWN],
|
|
906
949
|
['責任者', ownerValue(item.owner)],
|
|
907
950
|
['有効状態', lifecycleStatusLabel(item.lifecycle.status)],
|
|
908
951
|
['適用', item.applicability.state ? applicabilityStateLabel(item.applicability.state) : UNKNOWN],
|
|
@@ -1383,6 +1426,212 @@ function defaultCandidateBody(values, context = {}) {
|
|
|
1383
1426
|
};
|
|
1384
1427
|
}
|
|
1385
1428
|
|
|
1429
|
+
function projectOptions(projects) {
|
|
1430
|
+
const list = typeof projects === 'function' ? projects() : projects;
|
|
1431
|
+
if (!Array.isArray(list)) return [];
|
|
1432
|
+
const options = [];
|
|
1433
|
+
for (const project of list) {
|
|
1434
|
+
const code = optionalText(fieldValue(project, 'code', 'project_code', 'projectCode') ?? (typeof project === 'string' ? project : '')).trim();
|
|
1435
|
+
if (!code || options.some((option) => option.code === code)) continue;
|
|
1436
|
+
const name = optionalText(fieldValue(project, 'name', 'display_name', 'displayName', 'label')).trim();
|
|
1437
|
+
options.push({ code, name: name || code });
|
|
1438
|
+
}
|
|
1439
|
+
return options;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Renders the optional project filter shared by the organization-wide list
|
|
1444
|
+
* and the single project workspace. The empty value means every project the
|
|
1445
|
+
* viewer may read; it never stands for an implicitly chosen project.
|
|
1446
|
+
*/
|
|
1447
|
+
export function renderKnowledgeProjectFilter(root, model = {}, callbacks = {}) {
|
|
1448
|
+
clear(root);
|
|
1449
|
+
const label = makeElement('label', { className: 'knowledge-project-filter', text: 'プロジェクト' });
|
|
1450
|
+
const select = makeElement('select', { className: 'knowledge-control', attrs: { name: 'knowledge_project', 'aria-label': 'プロジェクトで絞り込む' } });
|
|
1451
|
+
const selected = optionalText(model.selected).trim();
|
|
1452
|
+
const options = projectOptions(model.projects);
|
|
1453
|
+
append(select, makeElement('option', { text: 'すべてのプロジェクト', value: '', attrs: { value: '', ...(selected ? {} : { selected: 'selected' }) } }));
|
|
1454
|
+
for (const option of options) {
|
|
1455
|
+
append(select, makeElement('option', { text: option.name === option.code ? option.code : `${option.name}(${option.code})`, value: option.code, attrs: { value: option.code, ...(option.code === selected ? { selected: 'selected' } : {}) } }));
|
|
1456
|
+
}
|
|
1457
|
+
if (selected && !options.some((option) => option.code === selected)) {
|
|
1458
|
+
append(select, makeElement('option', { text: selected, value: selected, attrs: { value: selected, selected: 'selected' } }));
|
|
1459
|
+
}
|
|
1460
|
+
select.value = selected;
|
|
1461
|
+
select.addEventListener('change', () => callbacks.onChange?.(optionalText(select.value).trim() || null));
|
|
1462
|
+
label.append(select);
|
|
1463
|
+
root.append(label);
|
|
1464
|
+
return label;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* Creates the organization-wide knowledge list. It reads every project the
|
|
1469
|
+
* viewer may read plus organization-wide records from one host path, shows
|
|
1470
|
+
* the project each record belongs to, and only reads. Registration and
|
|
1471
|
+
* version changes stay in the single project workspace: choosing a project
|
|
1472
|
+
* calls `onSelectProject`, and the host mounts `createKnowledgeOutcomeController`.
|
|
1473
|
+
*
|
|
1474
|
+
* @param {object} options
|
|
1475
|
+
* @param {string} [options.itemsPath] Host path for the organization-wide list.
|
|
1476
|
+
* @param {Array|Function} [options.projects] Projects offered in the filter.
|
|
1477
|
+
* @param {(code: string|null, context?: object) => void} [options.onSelectProject]
|
|
1478
|
+
*/
|
|
1479
|
+
export function createKnowledgeOverviewController(options = {}) {
|
|
1480
|
+
const { root, api, onSelectProject } = options;
|
|
1481
|
+
if (!root || typeof root.replaceChildren !== 'function') throw new Error('knowledge_root_unavailable');
|
|
1482
|
+
const labels = resolveKnowledgeUiLabels(options.labels);
|
|
1483
|
+
const itemsBase = optionalText(options.itemsPath).trim() || `/api/knowledge/${KNOWLEDGE_ROUTES.items}`;
|
|
1484
|
+
const state = {
|
|
1485
|
+
filters: { q: '', scope: 'all', status: 'active' },
|
|
1486
|
+
list: { state: 'loading', records: null, absence_confirmed: false, searched_scope: null },
|
|
1487
|
+
selected: null,
|
|
1488
|
+
detail: { state: 'idle' },
|
|
1489
|
+
};
|
|
1490
|
+
let selectionGeneration = 0;
|
|
1491
|
+
const projectName = (code) => projectOptions(options.projects).find((option) => option.code === code)?.name ?? null;
|
|
1492
|
+
|
|
1493
|
+
function itemsPath() {
|
|
1494
|
+
const query = new URLSearchParams();
|
|
1495
|
+
if (state.filters.q) query.set('q', state.filters.q);
|
|
1496
|
+
if (state.filters.scope) query.set('scope', state.filters.scope);
|
|
1497
|
+
if (state.filters.status) query.set('status', state.filters.status);
|
|
1498
|
+
const encoded = query.toString();
|
|
1499
|
+
return `${itemsBase}${encoded ? `?${encoded}` : ''}`;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function emptyText() {
|
|
1503
|
+
const filtered = Boolean(state.filters.q) || state.filters.scope !== 'all' || state.filters.status !== 'active';
|
|
1504
|
+
return filtered
|
|
1505
|
+
? 'この条件に一致する知識・判断はありません。'
|
|
1506
|
+
: '見られる範囲に、有効な知識・判断の記録はありません。';
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function scopeText(collection) {
|
|
1510
|
+
const count = Array.isArray(collection.searched_scope) ? collection.searched_scope.length : null;
|
|
1511
|
+
return count === null
|
|
1512
|
+
? '読んだ範囲: 未確認'
|
|
1513
|
+
: `読んだ範囲: 見られる${count}件のプロジェクトと組織共通`;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function openProject(code, context = {}) {
|
|
1517
|
+
if (!code) return;
|
|
1518
|
+
onSelectProject?.(code, context);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function render() {
|
|
1522
|
+
clear(root);
|
|
1523
|
+
const shell = makeElement('div', { className: 'knowledge-outcome knowledge-overview' });
|
|
1524
|
+
const header = makeElement('header', { className: 'knowledge-outcome-head' });
|
|
1525
|
+
const headerMain = makeElement('div', { className: 'knowledge-header-main' });
|
|
1526
|
+
append(headerMain,
|
|
1527
|
+
makeElement('span', { className: 'knowledge-eyebrow', text: 'Brainbase Outcome' }),
|
|
1528
|
+
makeElement('h2', { text: '知識・判断' }),
|
|
1529
|
+
makeElement('p', { text: '見られるすべてのプロジェクトと組織共通の知識・判断を一覧にします。登録や改訂は、プロジェクトを選ぶとできます。' }),
|
|
1530
|
+
);
|
|
1531
|
+
const headerMeta = makeElement('div', { className: 'knowledge-header-meta' });
|
|
1532
|
+
renderKnowledgeProjectFilter(headerMeta, { projects: options.projects, selected: null }, { onChange: (code) => openProject(code, { reason: 'filter' }) });
|
|
1533
|
+
append(header, headerMain, headerMeta);
|
|
1534
|
+
shell.append(header);
|
|
1535
|
+
const body = makeElement('div', { className: 'knowledge-outcome-body knowledge-workspace', attrs: { 'data-view': 'discovery' } });
|
|
1536
|
+
const primary = makeElement('section', { className: 'knowledge-outcome-primary knowledge-master', attrs: { 'aria-label': '知識の一覧' } });
|
|
1537
|
+
const discovery = makeElement('section', { className: 'knowledge-discovery-view', attrs: { 'aria-labelledby': 'knowledge-discovery-title' } });
|
|
1538
|
+
const discoveryHeader = makeElement('div', { className: 'knowledge-view-heading' });
|
|
1539
|
+
append(discoveryHeader, makeElement('div', {},), makeElement('span', { className: 'knowledge-view-kicker', text: labels.discovery }));
|
|
1540
|
+
discoveryHeader.firstChild.append(makeElement('h3', { id: 'knowledge-discovery-title', text: '知識・判断を探す' }), makeElement('p', { text: '選んだ記録の本文・出典・所属プロジェクトを確認できます。' }));
|
|
1541
|
+
discovery.append(discoveryHeader);
|
|
1542
|
+
const filters = makeElement('div', { className: 'knowledge-filter-region knowledge-discovery-toolbar' });
|
|
1543
|
+
renderDiscoveryFilters(filters, state.filters, {
|
|
1544
|
+
scopeLabels: { project: 'プロジェクトの記録', organization: '組織共通', all: 'すべて' },
|
|
1545
|
+
onChange: (next) => { state.filters = next; void loadItems(); },
|
|
1546
|
+
});
|
|
1547
|
+
discovery.append(filters);
|
|
1548
|
+
const list = makeElement('div', { className: 'knowledge-discovery-list' });
|
|
1549
|
+
renderKnowledgeList(list, state.list, {
|
|
1550
|
+
labels,
|
|
1551
|
+
showProject: true,
|
|
1552
|
+
projectLabel: projectName,
|
|
1553
|
+
scopeText,
|
|
1554
|
+
emptyText: emptyText(),
|
|
1555
|
+
selectedId: state.selected?.id,
|
|
1556
|
+
onSelect: (item) => { void selectItem(item); },
|
|
1557
|
+
onRetry: () => loadItems(),
|
|
1558
|
+
});
|
|
1559
|
+
discovery.append(list);
|
|
1560
|
+
primary.append(discovery);
|
|
1561
|
+
body.append(primary);
|
|
1562
|
+
const detail = makeElement('aside', { className: 'knowledge-outcome-detail knowledge-inspector', attrs: { 'aria-label': labels.inspector, id: 'knowledge-selected-detail', tabindex: '-1' } });
|
|
1563
|
+
const inspectorHeading = makeElement('div', { className: 'knowledge-inspector-heading' });
|
|
1564
|
+
append(inspectorHeading, makeElement('span', { className: 'knowledge-view-kicker', text: labels.inspector }), makeElement('span', { className: 'knowledge-inspector-state', text: state.selected ? labels.selected : labels.unselected }));
|
|
1565
|
+
detail.append(inspectorHeading);
|
|
1566
|
+
const detailContent = makeElement('div', { className: 'knowledge-inspector-detail' });
|
|
1567
|
+
const hasSelectableKnowledge = normaliseStatus(state.list.state) === 'ready' && Array.isArray(state.list.records) && state.list.records.length > 0;
|
|
1568
|
+
renderKnowledgeDetail(detailContent, state.selected, {
|
|
1569
|
+
labels,
|
|
1570
|
+
detailState: state.detail,
|
|
1571
|
+
scopeLabels: { project: 'プロジェクトの記録', organization: '組織共通' },
|
|
1572
|
+
showSelectionGuide: Boolean(state.selected) || hasSelectableKnowledge,
|
|
1573
|
+
onRetry: state.selected ? () => void selectItem(state.selected) : null,
|
|
1574
|
+
});
|
|
1575
|
+
detail.append(detailContent);
|
|
1576
|
+
if (state.selected) {
|
|
1577
|
+
const owner = makeElement('div', { className: 'knowledge-overview-owner' });
|
|
1578
|
+
const code = optionalText(state.selected.project_code).trim();
|
|
1579
|
+
append(owner, makeElement('p', { className: 'knowledge-overview-project', text: `所属: ${knowledgeItemProjectLabel(state.selected, projectName)}` }));
|
|
1580
|
+
if (code) {
|
|
1581
|
+
const open = makeElement('button', { className: 'knowledge-button secondary', text: 'このプロジェクトで開く', attrs: { type: 'button' } });
|
|
1582
|
+
open.addEventListener('click', () => openProject(code, { reason: 'record', item_id: state.selected.id }));
|
|
1583
|
+
owner.append(open);
|
|
1584
|
+
}
|
|
1585
|
+
detail.append(owner);
|
|
1586
|
+
}
|
|
1587
|
+
body.append(detail); shell.append(body); root.append(shell);
|
|
1588
|
+
options.onStateChange?.(state);
|
|
1589
|
+
return shell;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
async function loadItems() {
|
|
1593
|
+
selectionGeneration += 1;
|
|
1594
|
+
state.selected = null;
|
|
1595
|
+
state.detail = { state: 'idle' };
|
|
1596
|
+
state.list = { ...state.list, state: 'loading' }; render();
|
|
1597
|
+
try { state.list = normalizeKnowledgeCollection(await callKnowledgeApi(api, itemsPath())); }
|
|
1598
|
+
catch (error) { state.list = { state: error.code === 'permission_denied' || error.status === 403 ? 'permission_denied' : 'error_retryable', records: null, message: apiErrorText(error), error: error.code }; }
|
|
1599
|
+
render(); return state.list;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
async function selectItem(item) {
|
|
1603
|
+
const requestGeneration = ++selectionGeneration;
|
|
1604
|
+
state.selected = item ?? null;
|
|
1605
|
+
const code = optionalText(item?.project_code).trim();
|
|
1606
|
+
if (!item?.id || !code) {
|
|
1607
|
+
// A record whose project is unknown cannot be read through a project
|
|
1608
|
+
// route. Show what the list returned and say the canonical read is
|
|
1609
|
+
// unavailable instead of presenting it as the confirmed detail.
|
|
1610
|
+
state.detail = item ? { state: 'unknown', message: '所属プロジェクトを確認できないため、正本の詳細を読めません。' } : { state: 'idle' };
|
|
1611
|
+
render(); return null;
|
|
1612
|
+
}
|
|
1613
|
+
state.detail = { state: 'loading' }; render();
|
|
1614
|
+
try {
|
|
1615
|
+
const path = typeof options.detailPath === 'function'
|
|
1616
|
+
? options.detailPath(code, item.id)
|
|
1617
|
+
: knowledgePath(code, `${KNOWLEDGE_ROUTES.items}/${encodeURIComponent(item.id)}`);
|
|
1618
|
+
const payload = await callKnowledgeApi(api, path);
|
|
1619
|
+
if (requestGeneration !== selectionGeneration) return null;
|
|
1620
|
+
const read = normalizeKnowledgeItem(payload?.item ?? payload?.record ?? payload?.data ?? payload);
|
|
1621
|
+
const detailed = read ? { ...read, project_code: read.project_code ?? code } : item;
|
|
1622
|
+
state.selected = detailed; state.detail = { state: 'ready', item: detailed }; render(); return detailed;
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
if (requestGeneration !== selectionGeneration) return null;
|
|
1625
|
+
state.detail = { state: error.code === 'permission_denied' || error.status === 403 ? 'permission_denied' : 'error_retryable', message: apiErrorText(error), error: error.code }; render(); return null;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
const controller = { state, render, loadItems, selectItem, paths: { items: itemsPath } };
|
|
1630
|
+
render();
|
|
1631
|
+
if (options.autoLoad !== false) void loadItems();
|
|
1632
|
+
return controller;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1386
1635
|
/**
|
|
1387
1636
|
* Creates the knowledge outcome workspace.
|
|
1388
1637
|
*
|
|
@@ -1452,7 +1701,14 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1452
1701
|
makeElement('p', { text: '正本を探し、由来と状態を確認して、必要なときだけ更新します。' }),
|
|
1453
1702
|
);
|
|
1454
1703
|
const headerMeta = makeElement('div', { className: 'knowledge-header-meta' });
|
|
1455
|
-
|
|
1704
|
+
if (options.projectFilter) {
|
|
1705
|
+
// The host decides what choosing another project, or every project,
|
|
1706
|
+
// means. Showing the filter keeps the chosen project explicit instead
|
|
1707
|
+
// of a fixed label that cannot be changed from here.
|
|
1708
|
+
renderKnowledgeProjectFilter(headerMeta, { projects: options.projectFilter.projects, selected: code }, { onChange: (next) => options.projectFilter.onChange?.(next) });
|
|
1709
|
+
} else {
|
|
1710
|
+
headerMeta.append(makeElement('span', { className: 'knowledge-project-context', text: `プロジェクト: ${code}` }));
|
|
1711
|
+
}
|
|
1456
1712
|
append(header, headerMain, headerMeta);
|
|
1457
1713
|
const nav = makeElement('nav', { className: 'knowledge-outcome-nav', attrs: { 'aria-label': '知識の操作' } });
|
|
1458
1714
|
for (const [value, label] of [['discovery', '探す'], ['capture', '登録案を作る'], ['preview', '公開前テスト'], ['destination', '保存先']]) {
|
|
@@ -108,10 +108,20 @@ function linkLine(doc, link, label, extra) {
|
|
|
108
108
|
return li;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/** The record fields a gap is read from, in the words of the task screen. */
|
|
112
|
+
const SOURCE_FIELD_TEXT = Object.freeze({
|
|
113
|
+
assignee_person_id: '担当欄',
|
|
114
|
+
description: '本文',
|
|
115
|
+
source_refs: '出典リンク',
|
|
116
|
+
review_at: '見直し予定',
|
|
117
|
+
updated_at: '最終更新',
|
|
118
|
+
relation: '関係',
|
|
119
|
+
});
|
|
120
|
+
|
|
111
121
|
function sourceText(source, readAt) {
|
|
112
122
|
if (!source) return null;
|
|
113
123
|
const system = source.system === 'task_api' ? 'タスクの記録' : '組織のGraph';
|
|
114
|
-
const fields = source.fields?.length && !source.fields.includes('*') ? `の ${source.fields.join('・')}` : '';
|
|
124
|
+
const fields = source.fields?.length && !source.fields.includes('*') ? `の ${source.fields.map((field) => SOURCE_FIELD_TEXT[field] ?? '記録の欄').join('・')}` : '';
|
|
115
125
|
const at = readAt ? `(${shortTime(readAt)}に読み込み)` : '';
|
|
116
126
|
return `${system}${fields}${at}`;
|
|
117
127
|
}
|
|
@@ -332,7 +342,10 @@ export function workSiteBlocks(doc, { business, site, readAt, taskHref }) {
|
|
|
332
342
|
? [el(doc, 'a', { className: 'bb-world-rail-link', text: '「タスク」でこの仕事を開く', attrs: { href } }), { text: '担当・期限・状態はタスク画面で直せます。保存先はタスクの正本で、世界にはもう一度読むと反映されます。本文の人物を担当欄へ接続するのも、そこで行います。', className: 'bb-world-rail-note' }]
|
|
333
343
|
: { text: 'この画面からは直せません。タスクを管理している画面で直してください。', className: 'bb-world-rail-note' },
|
|
334
344
|
}));
|
|
335
|
-
|
|
345
|
+
// The task's id and version are for tracing, so they wait behind the record details.
|
|
346
|
+
const record = el(doc, 'details', { className: 'bb-world-original' });
|
|
347
|
+
record.append(el(doc, 'summary', { text: '記録の詳細' }), workspaceDefinition(doc, [['タスクID', site.task_id], ['版', site.source.version ?? '不明']]));
|
|
348
|
+
blocks.push(workspaceRailBlock(doc, { title: '出典', content: [workspaceDefinition(doc, [['記録', 'タスクの記録'], ['事業', business.name]]), record] }));
|
|
336
349
|
return blocks;
|
|
337
350
|
}
|
|
338
351
|
|