@unson/brainbase-mcp 0.23.1 → 0.23.3
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/company-os-judgment-history.d.ts +157 -0
- package/dist/company-os-judgment-history.js +489 -0
- package/dist/judgment-history-http.d.ts +18 -0
- package/dist/judgment-history-http.js +170 -0
- package/dist/judgment-history.d.ts +178 -0
- package/dist/judgment-history.js +1519 -0
- package/dist/knowledge-continuation.js +1 -1
- package/dist/knowledge-lookup.js +1 -1
- package/dist/local-web-host.d.ts +8 -0
- package/dist/local-web-host.js +36 -0
- package/dist/problem-selection.d.ts +84 -1
- package/dist/problem-selection.js +141 -15
- package/package.json +14 -2
- package/ui/judgment-history.css +173 -0
- package/ui/judgment-history.js +1077 -32
- package/ui/local-web-shell.js +1 -1
package/ui/judgment-history.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
*
|
|
2
|
+
* Judgment history projection for the local and organization workspaces.
|
|
3
3
|
*
|
|
4
4
|
* The host already owns the journal read model. This module deliberately
|
|
5
5
|
* projects that model in the browser instead of creating another ledger. It
|
|
@@ -28,6 +28,20 @@ const JST_OFFSET_MS = 9 * 60 * 60 * 1000;
|
|
|
28
28
|
const PERIODS = Object.freeze({ week: 'week', past30days: 'past30days', all: 'all' });
|
|
29
29
|
const PERIOD_LABELS = Object.freeze({ week: '今週', past30days: '過去30日', all: '全期間' });
|
|
30
30
|
const SECTION_KEYS = Object.freeze(['needs_human', 'blocked', 'continued', 'other']);
|
|
31
|
+
const NORMAL_ENTRYPOINTS = Object.freeze(['codex', 'claude_code', 'mana', 'company_os', 'unknown']);
|
|
32
|
+
const NORMAL_ENTRYPOINT_LABELS = Object.freeze({
|
|
33
|
+
codex: 'Codex',
|
|
34
|
+
claude_code: 'Claude Code',
|
|
35
|
+
mana: 'Mana',
|
|
36
|
+
company_os: 'Company OS',
|
|
37
|
+
unknown: '入口不明',
|
|
38
|
+
});
|
|
39
|
+
const FEEDBACK_KINDS = Object.freeze(['feedback', 'result', 'correction']);
|
|
40
|
+
const FEEDBACK_KIND_LABELS = Object.freeze({
|
|
41
|
+
feedback: 'フィードバック',
|
|
42
|
+
result: '結果',
|
|
43
|
+
correction: '訂正',
|
|
44
|
+
});
|
|
31
45
|
|
|
32
46
|
function isRecord(value) {
|
|
33
47
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -37,6 +51,63 @@ function text(value) {
|
|
|
37
51
|
return typeof value === 'string' ? value.trim() : '';
|
|
38
52
|
}
|
|
39
53
|
|
|
54
|
+
function canonicalJson(value) {
|
|
55
|
+
if (value === null || typeof value !== 'object') {
|
|
56
|
+
if (typeof value === 'number' && !Number.isFinite(value)) return JSON.stringify(null);
|
|
57
|
+
return JSON.stringify(value);
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(value)) return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`;
|
|
60
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function feedbackKind(value) {
|
|
64
|
+
const candidate = text(value).toLowerCase();
|
|
65
|
+
return FEEDBACK_KINDS.includes(candidate) ? candidate : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeFeedbackEvent(value, fallbackRecordId) {
|
|
69
|
+
if (!isRecord(value)) return null;
|
|
70
|
+
const eventId = text(value.event_id ?? value.eventId ?? value.id);
|
|
71
|
+
const recordId = text(value.record_id ?? value.recordId ?? fallbackRecordId);
|
|
72
|
+
const kind = feedbackKind(value.kind ?? value.type);
|
|
73
|
+
if (!eventId || !recordId || !kind || !isRecord(value.content)) return null;
|
|
74
|
+
return {
|
|
75
|
+
recordId,
|
|
76
|
+
eventId,
|
|
77
|
+
kind,
|
|
78
|
+
content: value.content,
|
|
79
|
+
recordedAt: text(value.recorded_at ?? value.recordedAt) || null,
|
|
80
|
+
saved: value.saved === true,
|
|
81
|
+
storageStatus: text(value.storage_status ?? value.storageStatus ?? value.save_status).toLowerCase() || null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeFeedbackEvents(value, fallbackRecordId) {
|
|
86
|
+
if (!isRecord(value)) return [];
|
|
87
|
+
const candidates = [
|
|
88
|
+
value.feedback_events,
|
|
89
|
+
value.feedback,
|
|
90
|
+
isRecord(value.feedback) ? value.feedback.events : undefined,
|
|
91
|
+
value.events,
|
|
92
|
+
value.appended_events,
|
|
93
|
+
value.event,
|
|
94
|
+
value.feedback_event,
|
|
95
|
+
];
|
|
96
|
+
const collection = candidates.find((candidate) => Array.isArray(candidate))
|
|
97
|
+
?? candidates.find((candidate) => isRecord(candidate));
|
|
98
|
+
if (!collection) return [];
|
|
99
|
+
const entries = Array.isArray(collection) ? collection : [collection];
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
const result = [];
|
|
102
|
+
for (const candidate of entries) {
|
|
103
|
+
const event = normalizeFeedbackEvent(candidate, fallbackRecordId);
|
|
104
|
+
if (!event || seen.has(event.eventId)) continue;
|
|
105
|
+
seen.add(event.eventId);
|
|
106
|
+
result.push(event);
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
40
111
|
function findFirst(node, predicate) {
|
|
41
112
|
if (!node) return null;
|
|
42
113
|
if (predicate(node)) return node;
|
|
@@ -87,9 +158,21 @@ function normalizeCoverage(coverage, status) {
|
|
|
87
158
|
const complete = source.complete !== false && state !== 'partial' && state !== 'unknown';
|
|
88
159
|
const saved = nonNegativeInteger(source.saved ?? source.read ?? source.count);
|
|
89
160
|
const rejected = nonNegativeInteger(source.rejected ?? source.rejected_count);
|
|
161
|
+
const total = nonNegativeInteger(source.total);
|
|
162
|
+
const storage = text(source.storage).toLowerCase() || null;
|
|
163
|
+
const sources = Array.isArray(source.sources)
|
|
164
|
+
? source.sources.filter(isRecord).map((entry) => ({
|
|
165
|
+
entrypoint: text(entry.entrypoint) || 'unknown',
|
|
166
|
+
status: text(entry.status) || 'unknown',
|
|
167
|
+
reason: text(entry.reason) || null,
|
|
168
|
+
}))
|
|
169
|
+
: [];
|
|
90
170
|
return {
|
|
91
171
|
saved,
|
|
92
172
|
rejected,
|
|
173
|
+
total,
|
|
174
|
+
storage,
|
|
175
|
+
sources,
|
|
93
176
|
latestRecordedAt: text(source.latest_recorded_at ?? source.latestRecordedAt) || null,
|
|
94
177
|
possiblyStalled: source.possibly_stalled === true || source.possiblyStalled === true,
|
|
95
178
|
complete: status !== 'partial' && complete,
|
|
@@ -98,6 +181,124 @@ function normalizeCoverage(coverage, status) {
|
|
|
98
181
|
};
|
|
99
182
|
}
|
|
100
183
|
|
|
184
|
+
function normalEntrypoint(value) {
|
|
185
|
+
const candidate = text(value).toLowerCase();
|
|
186
|
+
return NORMAL_ENTRYPOINTS.includes(candidate) ? candidate : 'unknown';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeNormalReference(value) {
|
|
190
|
+
if (!isRecord(value)) return null;
|
|
191
|
+
const ref = text(value.ref ?? value.entity_id ?? value.id);
|
|
192
|
+
if (!ref) return null;
|
|
193
|
+
return {
|
|
194
|
+
ref,
|
|
195
|
+
id: ref,
|
|
196
|
+
kind: text(value.kind) || null,
|
|
197
|
+
version: text(value.version ?? value.revision) || null,
|
|
198
|
+
digest: text(value.digest) || null,
|
|
199
|
+
why: text(value.why) || null,
|
|
200
|
+
usage: text(value.usage) || null,
|
|
201
|
+
availability: text(value.availability).toLowerCase() || 'unknown',
|
|
202
|
+
label: text(value.label ?? value.name) || ref,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function normalizeNormalAlternative(value) {
|
|
207
|
+
if (!isRecord(value)) return null;
|
|
208
|
+
const adopted = typeof value.adopted === 'boolean' ? value.adopted : null;
|
|
209
|
+
return {
|
|
210
|
+
label: text(value.label ?? value.summary ?? value.option) || null,
|
|
211
|
+
evaluation: text(value.evaluation ?? value.impact ?? value.reason) || null,
|
|
212
|
+
adoption: text(value.adoption ?? value.adoption_status ?? value.selected) || null,
|
|
213
|
+
adopted,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function normalizeNormalRecord(value) {
|
|
218
|
+
if (!isRecord(value)) return null;
|
|
219
|
+
const recordId = text(value.record_id);
|
|
220
|
+
const recordedAt = text(value.recorded_at);
|
|
221
|
+
if (!recordId || timestamp(recordedAt) === null) return null;
|
|
222
|
+
const judgment = isRecord(value.judgment) ? value.judgment : {};
|
|
223
|
+
const execution = isRecord(value.execution) ? value.execution : {};
|
|
224
|
+
const invalidFields = [];
|
|
225
|
+
const normalizeCollection = (value, field, normalize) => {
|
|
226
|
+
if (value === null || value === undefined) return null;
|
|
227
|
+
if (!Array.isArray(value)) {
|
|
228
|
+
invalidFields.push(field);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const normalized = value.map(normalize);
|
|
232
|
+
if (normalized.some((entry) => entry === null)) invalidFields.push(field);
|
|
233
|
+
return normalized.filter(Boolean);
|
|
234
|
+
};
|
|
235
|
+
const selectedReferences = normalizeCollection(judgment.selected_references, 'judgment.selected_references', normalizeNormalReference);
|
|
236
|
+
const alternatives = normalizeCollection(judgment.alternatives, 'judgment.alternatives', normalizeNormalAlternative);
|
|
237
|
+
const missingFields = Array.isArray(value.missing_fields)
|
|
238
|
+
? value.missing_fields.map((field) => text(field)).filter(Boolean)
|
|
239
|
+
: [];
|
|
240
|
+
for (const field of invalidFields) {
|
|
241
|
+
if (!missingFields.includes(field)) missingFields.push(field);
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
record: value,
|
|
245
|
+
recordId,
|
|
246
|
+
entrypoint: normalEntrypoint(value.entrypoint),
|
|
247
|
+
recordedAt,
|
|
248
|
+
projectCode: value.project_code === null || value.project_code === undefined ? null : text(value.project_code) || null,
|
|
249
|
+
turnRef: value.turn_ref === null || value.turn_ref === undefined ? null : text(value.turn_ref) || null,
|
|
250
|
+
judgment,
|
|
251
|
+
execution,
|
|
252
|
+
selectedReferences,
|
|
253
|
+
alternatives,
|
|
254
|
+
feedbackEvents: normalizeFeedbackEvents(value, recordId),
|
|
255
|
+
missingFields,
|
|
256
|
+
invalidFields,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeNormalFilters(filters) {
|
|
261
|
+
if (!isRecord(filters)) return { period: null, project: null, entrypoint: null, projects: [], entrypoints: [] };
|
|
262
|
+
const normalizeOptions = (value) => {
|
|
263
|
+
if (!Array.isArray(value)) return [];
|
|
264
|
+
return value.map((entry) => {
|
|
265
|
+
if (typeof entry === 'string') return { value: entry, label: entry };
|
|
266
|
+
if (!isRecord(entry)) return null;
|
|
267
|
+
const option = text(entry.value ?? entry.code ?? entry.project ?? entry.entrypoint ?? entry.id);
|
|
268
|
+
return option ? { value: option, label: text(entry.label) || option } : null;
|
|
269
|
+
}).filter(Boolean);
|
|
270
|
+
};
|
|
271
|
+
const projects = normalizeOptions(filters.projects ?? filters.project_options ?? filters.projectCodes);
|
|
272
|
+
const entrypoints = normalizeOptions(filters.entrypoints ?? filters.entrypoint_options)
|
|
273
|
+
.filter((entry) => NORMAL_ENTRYPOINTS.includes(entry.value));
|
|
274
|
+
return {
|
|
275
|
+
period: text(filters.period) || null,
|
|
276
|
+
project: filters.project === null || filters.project === undefined ? null : text(filters.project) || null,
|
|
277
|
+
entrypoint: filters.entrypoint === null || filters.entrypoint === undefined ? null : normalEntrypoint(filters.entrypoint),
|
|
278
|
+
projects,
|
|
279
|
+
entrypoints,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function normalizePagination(pagination) {
|
|
284
|
+
if (!isRecord(pagination)) return { nextCursor: null, previousCursor: null, limit: null, hasNext: false };
|
|
285
|
+
const nextCursor = text(pagination.next_cursor ?? pagination.nextCursor) || null;
|
|
286
|
+
const previousCursor = text(pagination.previous_cursor ?? pagination.previousCursor) || null;
|
|
287
|
+
const limit = nonNegativeInteger(pagination.limit);
|
|
288
|
+
return {
|
|
289
|
+
nextCursor,
|
|
290
|
+
previousCursor,
|
|
291
|
+
limit,
|
|
292
|
+
hasNext: pagination.has_next === true || pagination.hasNext === true || Boolean(nextCursor),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function isNormalHistoryPayload(payload) {
|
|
297
|
+
return payload?.contract_version === 'brainbase.judgment-history.v1'
|
|
298
|
+
|| Array.isArray(payload?.records)
|
|
299
|
+
|| isRecord(payload?.record);
|
|
300
|
+
}
|
|
301
|
+
|
|
101
302
|
function isProof(value) {
|
|
102
303
|
return isRecord(value)
|
|
103
304
|
&& text(value.intent_id)
|
|
@@ -140,18 +341,57 @@ function normalizeSectionItems(sections) {
|
|
|
140
341
|
*/
|
|
141
342
|
export function normalizeJudgmentHistoryHome(payload) {
|
|
142
343
|
if (!isRecord(payload)) return { status: 'invalid', reason: 'response_not_object' };
|
|
344
|
+
if (isNormalHistoryPayload(payload)) {
|
|
345
|
+
const rawStatus = text(payload.status).toLowerCase();
|
|
346
|
+
if (!['available', 'partial', 'unavailable', 'error'].includes(rawStatus)) {
|
|
347
|
+
return { mode: 'normal', status: 'invalid', reason: 'unsupported_status' };
|
|
348
|
+
}
|
|
349
|
+
const coverage = normalizeCoverage(payload.coverage, rawStatus);
|
|
350
|
+
if (rawStatus === 'unavailable' || rawStatus === 'error') {
|
|
351
|
+
return {
|
|
352
|
+
mode: 'normal',
|
|
353
|
+
status: rawStatus,
|
|
354
|
+
contractVersion: text(payload.contract_version) || 'brainbase.judgment-history.v1',
|
|
355
|
+
coverage,
|
|
356
|
+
filters: normalizeNormalFilters(payload.filters),
|
|
357
|
+
pagination: normalizePagination(payload.pagination),
|
|
358
|
+
records: [],
|
|
359
|
+
reason: text(payload.reason ?? coverage.reason) || null,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
if (!Array.isArray(payload.records)) return { mode: 'normal', status: 'invalid', reason: 'records_missing' };
|
|
363
|
+
const records = payload.records.map(normalizeNormalRecord).filter(Boolean);
|
|
364
|
+
const invalidRecordCount = records.filter((record) => record.invalidFields.length > 0).length;
|
|
365
|
+
const invalidCount = payload.records.length - records.length + invalidRecordCount;
|
|
366
|
+
const sourceGaps = coverage.sources.filter((source) => ['partial', 'unavailable'].includes(source.status));
|
|
367
|
+
const status = rawStatus === 'partial' || coverage.complete === false || invalidCount > 0 || sourceGaps.length > 0 ? 'partial' : 'available';
|
|
368
|
+
const reason = text(payload.reason ?? coverage.reason)
|
|
369
|
+
|| sourceGaps.find((source) => source.reason)?.reason
|
|
370
|
+
|| (invalidCount > 0 ? 'record_invalid' : null);
|
|
371
|
+
return {
|
|
372
|
+
mode: 'normal',
|
|
373
|
+
status,
|
|
374
|
+
contractVersion: text(payload.contract_version) || 'brainbase.judgment-history.v1',
|
|
375
|
+
coverage: { ...coverage, complete: status === 'available' && coverage.complete !== false },
|
|
376
|
+
filters: normalizeNormalFilters(payload.filters),
|
|
377
|
+
pagination: normalizePagination(payload.pagination),
|
|
378
|
+
records,
|
|
379
|
+
invalidCount,
|
|
380
|
+
reason,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
143
383
|
if (payload.status === 'unavailable') {
|
|
144
|
-
return { status: 'unavailable', root: text(payload.root) || null, reason: text(payload.reason) || 'judgment_journal_unavailable' };
|
|
384
|
+
return { mode: 'legacy', status: 'unavailable', root: text(payload.root) || null, reason: text(payload.reason) || 'judgment_journal_unavailable' };
|
|
145
385
|
}
|
|
146
386
|
if (payload.status !== 'available' && payload.status !== 'partial') {
|
|
147
|
-
return { status: 'invalid', reason: 'unsupported_status' };
|
|
387
|
+
return { mode: 'legacy', status: 'invalid', reason: 'unsupported_status' };
|
|
148
388
|
}
|
|
149
389
|
const sectionResult = normalizeSectionItems(payload.sections);
|
|
150
|
-
if (!sectionResult) return { status: 'invalid', reason: 'sections_missing' };
|
|
390
|
+
if (!sectionResult) return { mode: 'legacy', status: 'invalid', reason: 'sections_missing' };
|
|
151
391
|
const rawCoverage = isRecord(payload.coverage) ? payload.coverage : {};
|
|
152
392
|
const saved = nonNegativeInteger(rawCoverage.saved);
|
|
153
393
|
const rejectedCount = nonNegativeInteger(rawCoverage.rejected);
|
|
154
|
-
if (saved === null || rejectedCount === null) return { status: 'invalid', reason: 'coverage_incomplete' };
|
|
394
|
+
if (saved === null || rejectedCount === null) return { mode: 'legacy', status: 'invalid', reason: 'coverage_incomplete' };
|
|
155
395
|
const coverageState = text(rawCoverage.status ?? rawCoverage.state).toLowerCase();
|
|
156
396
|
const coverageMismatch = saved !== sectionResult.rawCount || rejectedCount !== (Array.isArray(payload.rejected) ? payload.rejected.length : 0);
|
|
157
397
|
const status = payload.status === 'partial' || rawCoverage.complete === false || coverageState === 'partial' || coverageState === 'unknown'
|
|
@@ -162,6 +402,7 @@ export function normalizeJudgmentHistoryHome(payload) {
|
|
|
162
402
|
? payload.rejected.filter(isRecord).map((entry) => ({ file: text(entry.file) || null, reason: text(entry.reason) || 'record_rejected' }))
|
|
163
403
|
: [];
|
|
164
404
|
return {
|
|
405
|
+
mode: 'legacy',
|
|
165
406
|
status,
|
|
166
407
|
root: text(payload.root) || null,
|
|
167
408
|
coverage: { ...normalizeCoverage(rawCoverage, status), saved, rejected: rejectedCount },
|
|
@@ -312,15 +553,148 @@ function isDelegated(proof) {
|
|
|
312
553
|
return proof?.interruption?.resolution === 'continued_without_human' && Boolean(text(proof?.decision?.summary));
|
|
313
554
|
}
|
|
314
555
|
|
|
556
|
+
function normalReferenceSearchText(reference) {
|
|
557
|
+
if (!isRecord(reference)) return '';
|
|
558
|
+
return [
|
|
559
|
+
reference.ref,
|
|
560
|
+
reference.kind,
|
|
561
|
+
reference.version,
|
|
562
|
+
reference.digest,
|
|
563
|
+
reference.why,
|
|
564
|
+
reference.usage,
|
|
565
|
+
reference.availability,
|
|
566
|
+
reference.label,
|
|
567
|
+
].filter(Boolean).join(' ');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function normalRecordSearchText(record) {
|
|
571
|
+
const judgment = isRecord(record.judgment) ? record.judgment : {};
|
|
572
|
+
const execution = isRecord(record.execution) ? record.execution : {};
|
|
573
|
+
const alternatives = Array.isArray(record.alternatives) ? record.alternatives : [];
|
|
574
|
+
const references = Array.isArray(record.selectedReferences) ? record.selectedReferences : [];
|
|
575
|
+
return [
|
|
576
|
+
record.recordId,
|
|
577
|
+
record.entrypoint,
|
|
578
|
+
NORMAL_ENTRYPOINT_LABELS[record.entrypoint],
|
|
579
|
+
record.projectCode,
|
|
580
|
+
record.turnRef,
|
|
581
|
+
record.recordedAt,
|
|
582
|
+
judgment.status,
|
|
583
|
+
judgment.summary,
|
|
584
|
+
judgment.reason,
|
|
585
|
+
execution.status,
|
|
586
|
+
execution.result_summary,
|
|
587
|
+
execution.outcome_status,
|
|
588
|
+
...record.missingFields,
|
|
589
|
+
...references.map(normalReferenceSearchText),
|
|
590
|
+
...alternatives.flatMap((alternative) => [alternative.label, alternative.evaluation, alternative.adoption, alternative.adopted]),
|
|
591
|
+
].filter(Boolean).join(' ').toLocaleLowerCase('ja-JP');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function aggregateNormalJudgmentHistory(home, { now = new Date(), period = PERIODS.past30days, query = '' } = {}) {
|
|
595
|
+
const empty = {
|
|
596
|
+
mode: 'normal',
|
|
597
|
+
rows: [],
|
|
598
|
+
stats: {
|
|
599
|
+
judgments: null,
|
|
600
|
+
references: null,
|
|
601
|
+
equivalent: null,
|
|
602
|
+
outcomes: null,
|
|
603
|
+
judgmentCount: null,
|
|
604
|
+
referenceCount: null,
|
|
605
|
+
equivalentJudgments: null,
|
|
606
|
+
confirmedOutcomes: null,
|
|
607
|
+
},
|
|
608
|
+
visibleCount: 0,
|
|
609
|
+
totalCount: home.coverage?.total ?? (home.coverage?.complete === true && home.records.length === 0 ? 0 : null),
|
|
610
|
+
period: normalizedPeriod(period),
|
|
611
|
+
query: text(query),
|
|
612
|
+
status: home.status,
|
|
613
|
+
coverage: home.coverage ?? null,
|
|
614
|
+
rejected: home.rejected ?? [],
|
|
615
|
+
reason: home.reason ?? null,
|
|
616
|
+
pagination: home.pagination ?? { nextCursor: null, previousCursor: null, limit: null, hasNext: false },
|
|
617
|
+
filters: home.filters ?? null,
|
|
618
|
+
};
|
|
619
|
+
if (home.status !== 'available' && home.status !== 'partial') return empty;
|
|
620
|
+
const nowMs = normalizedNow(now);
|
|
621
|
+
const start = periodStart(normalizedPeriod(period), nowMs);
|
|
622
|
+
const needle = text(query).toLocaleLowerCase('ja-JP');
|
|
623
|
+
const rows = [];
|
|
624
|
+
for (const record of home.records ?? []) {
|
|
625
|
+
const recorded = timestamp(record.recordedAt);
|
|
626
|
+
if (recorded === null || recorded < start || recorded > nowMs) continue;
|
|
627
|
+
const searchText = normalRecordSearchText(record);
|
|
628
|
+
if (needle && !searchText.includes(needle)) continue;
|
|
629
|
+
rows.push({
|
|
630
|
+
mode: 'normal',
|
|
631
|
+
key: record.recordId,
|
|
632
|
+
recordId: record.recordId,
|
|
633
|
+
recordedAt: record.recordedAt,
|
|
634
|
+
record: record.record,
|
|
635
|
+
entrypoint: record.entrypoint,
|
|
636
|
+
projectCode: record.projectCode,
|
|
637
|
+
turnRef: record.turnRef,
|
|
638
|
+
judgment: record.judgment,
|
|
639
|
+
execution: record.execution,
|
|
640
|
+
missingFields: record.missingFields,
|
|
641
|
+
alternatives: record.alternatives,
|
|
642
|
+
references: record.selectedReferences,
|
|
643
|
+
searchText,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
rows.sort((left, right) => {
|
|
647
|
+
const date = timestamp(right.recordedAt) - timestamp(left.recordedAt);
|
|
648
|
+
return date || right.recordId.localeCompare(left.recordId);
|
|
649
|
+
});
|
|
650
|
+
const references = new Map();
|
|
651
|
+
let referencesUnconfirmed = false;
|
|
652
|
+
for (const row of rows) {
|
|
653
|
+
if (row.references === null) {
|
|
654
|
+
referencesUnconfirmed = true;
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
for (const reference of row.references) {
|
|
658
|
+
const key = `${reference.ref}\u0000${reference.version ?? ''}\u0000${reference.digest ?? ''}`;
|
|
659
|
+
if (!references.has(key)) references.set(key, reference);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const metricsUnconfirmed = home.status === 'partial' && rows.length === 0;
|
|
663
|
+
const judgments = metricsUnconfirmed ? null : rows.length;
|
|
664
|
+
const referenceCount = metricsUnconfirmed || referencesUnconfirmed ? null : references.size;
|
|
665
|
+
const confirmedOutcomes = metricsUnconfirmed
|
|
666
|
+
? null
|
|
667
|
+
: rows.filter((row) => text(row.execution?.outcome_status).toLowerCase() === 'confirmed').length;
|
|
668
|
+
return {
|
|
669
|
+
...empty,
|
|
670
|
+
rows,
|
|
671
|
+
stats: {
|
|
672
|
+
judgments,
|
|
673
|
+
references: referenceCount,
|
|
674
|
+
equivalent: judgments,
|
|
675
|
+
outcomes: confirmedOutcomes,
|
|
676
|
+
delegated: judgments,
|
|
677
|
+
judgmentCount: judgments,
|
|
678
|
+
referenceCount,
|
|
679
|
+
equivalentJudgments: judgments,
|
|
680
|
+
confirmedOutcomes,
|
|
681
|
+
},
|
|
682
|
+
visibleCount: rows.length,
|
|
683
|
+
totalCount: home.coverage?.total ?? (home.coverage?.complete === true && home.records.length === 0 ? 0 : null),
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
315
687
|
/**
|
|
316
688
|
* Project a home read model into one newest-first judgment history.
|
|
317
689
|
* `now`, `period` and `query` are options so the same data can be tested and
|
|
318
690
|
* rendered consistently by a host.
|
|
319
691
|
*/
|
|
320
692
|
export function aggregateJudgmentHistory(home, { now = new Date(), period = PERIODS.past30days, query = '' } = {}) {
|
|
321
|
-
const normalized = home?.
|
|
693
|
+
const normalized = home?.mode === 'normal'
|
|
694
|
+
|| (home?.items && (home.status === 'available' || home.status === 'partial'))
|
|
322
695
|
? home
|
|
323
696
|
: normalizeJudgmentHistoryHome(home);
|
|
697
|
+
if (normalized.mode === 'normal') return aggregateNormalJudgmentHistory(normalized, { now, period, query });
|
|
324
698
|
const empty = {
|
|
325
699
|
rows: [],
|
|
326
700
|
stats: {
|
|
@@ -427,7 +801,8 @@ function formatDate(value) {
|
|
|
427
801
|
}
|
|
428
802
|
|
|
429
803
|
function makeReferenceSummary(doc, references) {
|
|
430
|
-
if (
|
|
804
|
+
if (references === null || references === undefined) return makeWorkspaceElement(doc, 'span', { className: 'bb-jh-unrecorded', text: '参照は未記録' });
|
|
805
|
+
if (!references.length) return makeWorkspaceElement(doc, 'span', { className: 'bb-jh-unrecorded', text: '参照はありません' });
|
|
431
806
|
const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-summary' });
|
|
432
807
|
const first = references.slice(0, 2).map((ref) => ref.label || ref.id).join('、');
|
|
433
808
|
wrap.append(makeWorkspaceElement(doc, 'strong', { text: first }));
|
|
@@ -436,6 +811,14 @@ function makeReferenceSummary(doc, references) {
|
|
|
436
811
|
}
|
|
437
812
|
|
|
438
813
|
function makeDecisionSummary(doc, row) {
|
|
814
|
+
if (row.mode === 'normal') {
|
|
815
|
+
const summary = text(row.judgment?.summary);
|
|
816
|
+
const status = text(row.judgment?.status);
|
|
817
|
+
const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-decision is-normal' });
|
|
818
|
+
wrap.append(makeWorkspaceElement(doc, 'strong', { text: summary || '判断内容は未記録' }));
|
|
819
|
+
wrap.append(makeWorkspaceElement(doc, 'small', { text: `${NORMAL_ENTRYPOINT_LABELS[row.entrypoint] ?? '入口不明'}${status ? `・${status}` : ''}` }));
|
|
820
|
+
return wrap;
|
|
821
|
+
}
|
|
439
822
|
const summary = text(row.proof?.decision?.summary);
|
|
440
823
|
const wrap = makeWorkspaceElement(doc, 'span', { className: `bb-jh-decision${row.delegated ? ' is-delegated' : ''}` });
|
|
441
824
|
wrap.append(makeWorkspaceElement(doc, 'strong', { text: summary || (row.proof?.interruption?.resolution === 'continued_without_human' ? '判断内容未記録' : '本人に確認') }));
|
|
@@ -445,13 +828,20 @@ function makeDecisionSummary(doc, row) {
|
|
|
445
828
|
}
|
|
446
829
|
|
|
447
830
|
function appendReferenceList(doc, references) {
|
|
448
|
-
if (
|
|
831
|
+
if (references === null || references === undefined) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '参照は記録されていません。' });
|
|
832
|
+
if (!references.length) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '参照はありません。' });
|
|
449
833
|
const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-reference-list' });
|
|
450
834
|
for (const ref of references) {
|
|
451
835
|
const item = makeWorkspaceElement(doc, 'li');
|
|
452
836
|
item.append(makeWorkspaceElement(doc, 'strong', { text: ref.label || ref.id }));
|
|
453
|
-
item.append(makeWorkspaceElement(doc, 'code', { text: `${ref.id}${ref.version ? `@${ref.version}` : ''}` }));
|
|
837
|
+
item.append(makeWorkspaceElement(doc, 'code', { text: `${ref.ref ?? ref.id}${ref.version ? `@${ref.version}` : ''}${ref.digest ? `#${ref.digest}` : ''}` }));
|
|
454
838
|
if (ref.application && ref.application !== ref.label) item.append(makeWorkspaceElement(doc, 'span', { text: ref.application }));
|
|
839
|
+
if (ref.availability && ref.availability !== 'recorded') {
|
|
840
|
+
const availability = ref.availability === 'permission_denied' ? '権限がありません' : ref.availability === 'unavailable' ? '現在は参照できません' : '参照状態は未確認';
|
|
841
|
+
item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-availability', text: availability }));
|
|
842
|
+
}
|
|
843
|
+
if (ref.why) item.append(makeWorkspaceElement(doc, 'span', { text: `理由: ${ref.why}` }));
|
|
844
|
+
if (ref.usage) item.append(makeWorkspaceElement(doc, 'span', { text: `用途: ${ref.usage}` }));
|
|
455
845
|
const alternateLabels = (ref.labels ?? []).filter((label) => label && label !== ref.label);
|
|
456
846
|
if (alternateLabels.length) item.append(makeWorkspaceElement(doc, 'span', { text: `別名: ${alternateLabels.join('、')}` }));
|
|
457
847
|
list.append(item);
|
|
@@ -470,6 +860,49 @@ function appendSupplement(doc, proof) {
|
|
|
470
860
|
]);
|
|
471
861
|
}
|
|
472
862
|
|
|
863
|
+
function appendNormalAlternatives(doc, alternatives) {
|
|
864
|
+
if (alternatives === null || alternatives === undefined) {
|
|
865
|
+
return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '案は記録されていません。' });
|
|
866
|
+
}
|
|
867
|
+
if (!alternatives.length) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '保存された案はありません。' });
|
|
868
|
+
const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-alternative-list' });
|
|
869
|
+
for (const alternative of alternatives) {
|
|
870
|
+
const item = makeWorkspaceElement(doc, 'li');
|
|
871
|
+
item.append(makeWorkspaceElement(doc, 'strong', { text: alternative.label || '案の名称は未記録' }));
|
|
872
|
+
if (alternative.evaluation) item.append(makeWorkspaceElement(doc, 'span', { text: `評価: ${alternative.evaluation}` }));
|
|
873
|
+
if (alternative.adoption) item.append(makeWorkspaceElement(doc, 'span', { text: `採用状態: ${alternative.adoption}` }));
|
|
874
|
+
else if (alternative.adopted === true) item.append(makeWorkspaceElement(doc, 'span', { text: '採用状態: 採用' }));
|
|
875
|
+
else if (alternative.adopted === false) item.append(makeWorkspaceElement(doc, 'span', { text: '採用状態: 不採用' }));
|
|
876
|
+
else if (Object.prototype.hasOwnProperty.call(alternative, 'adopted')) item.append(makeWorkspaceElement(doc, 'span', { text: '採用状態: 記録なし' }));
|
|
877
|
+
list.append(item);
|
|
878
|
+
}
|
|
879
|
+
return list;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function normalExecutionLabel(execution) {
|
|
883
|
+
const status = text(execution?.status).toLowerCase();
|
|
884
|
+
const labels = { pending: '実行待ち', completed: '実行済み', failed: '実行失敗', held: '保留', cancelled: '取消', unknown: '実行状態未確認' };
|
|
885
|
+
return labels[status] ?? '実行状態未確認';
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function normalOutcomeLabel(execution) {
|
|
889
|
+
const status = text(execution?.outcome_status).toLowerCase();
|
|
890
|
+
const labels = { confirmed: '結果確認済み', unconfirmed: '結果未確認', unknown: '結果未確認' };
|
|
891
|
+
return labels[status] ?? '結果未確認';
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function appendNormalExecution(doc, execution) {
|
|
895
|
+
return workspaceDefinition(doc, [
|
|
896
|
+
['実行', [normalExecutionLabel(execution), text(execution?.result_summary)]],
|
|
897
|
+
['結果', normalOutcomeLabel(execution)],
|
|
898
|
+
]);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function appendNormalMissingFields(doc, missingFields) {
|
|
902
|
+
if (!Array.isArray(missingFields) || missingFields.length === 0) return null;
|
|
903
|
+
return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-missing-fields', text: `未記録: ${missingFields.join('、')}` });
|
|
904
|
+
}
|
|
905
|
+
|
|
473
906
|
function appendTextParts(doc, values) {
|
|
474
907
|
const wrap = makeWorkspaceElement(doc, 'div');
|
|
475
908
|
for (const value of values) {
|
|
@@ -478,7 +911,209 @@ function appendTextParts(doc, values) {
|
|
|
478
911
|
return wrap;
|
|
479
912
|
}
|
|
480
913
|
|
|
481
|
-
function
|
|
914
|
+
function normalFeedbackContentSummary(content) {
|
|
915
|
+
if (!isRecord(content)) return '内容は未記録';
|
|
916
|
+
return text(content.summary ?? content.result_summary ?? content.message ?? content.reason)
|
|
917
|
+
|| canonicalJson(content)
|
|
918
|
+
|| '内容は未記録';
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function normalReferencePayload(record, event, applicabilityReason = '', eventRef = null) {
|
|
922
|
+
const payload = {
|
|
923
|
+
ref: `judgment-history:${record.recordId}`,
|
|
924
|
+
record_id: record.recordId,
|
|
925
|
+
why: text(applicabilityReason) || null,
|
|
926
|
+
};
|
|
927
|
+
if (event) {
|
|
928
|
+
payload.event_ref = eventRef;
|
|
929
|
+
payload.event_id = event.eventId;
|
|
930
|
+
payload.kind = event.kind;
|
|
931
|
+
}
|
|
932
|
+
return payload;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function normalReferencePayloadText(record, event, applicabilityReason = '', eventRef = null) {
|
|
936
|
+
return JSON.stringify(normalReferencePayload(record, event, applicabilityReason, eventRef), null, 2);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function appendNormalReferenceExport(doc, record, event, { onCopyReference } = {}) {
|
|
940
|
+
const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-reference-export' });
|
|
941
|
+
const refLabel = event
|
|
942
|
+
? `${record.recordId} / ${event.eventId}`
|
|
943
|
+
: `${record.recordId} / イベントIDは未記録`;
|
|
944
|
+
wrap.append(makeWorkspaceElement(doc, 'code', { text: refLabel }));
|
|
945
|
+
if (event) wrap.append(makeWorkspaceElement(doc, 'p', { text: `種別: ${FEEDBACK_KIND_LABELS[event.kind] ?? event.kind}・${normalFeedbackContentSummary(event.content)}` }));
|
|
946
|
+
const reason = makeWorkspaceElement(doc, 'textarea', {
|
|
947
|
+
className: 'bb-jh-applicability-reason',
|
|
948
|
+
attrs: {
|
|
949
|
+
rows: 2,
|
|
950
|
+
'aria-label': `適用理由${event ? `(${event.eventId})` : ''}`,
|
|
951
|
+
placeholder: '次の判断で使う理由を記入',
|
|
952
|
+
},
|
|
953
|
+
});
|
|
954
|
+
reason.value = '';
|
|
955
|
+
wrap.append(makeWorkspaceElement(doc, 'label', { text: '適用理由' }), reason);
|
|
956
|
+
const preview = makeWorkspaceElement(doc, 'pre', {
|
|
957
|
+
className: 'bb-jh-reference-payload',
|
|
958
|
+
text: normalReferencePayloadText(record, event),
|
|
959
|
+
});
|
|
960
|
+
const status = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-copy-status', text: 'この情報を次の判断へ渡せます。' });
|
|
961
|
+
wrap.append(preview, workspaceButton(doc, {
|
|
962
|
+
text: '参照情報をコピー',
|
|
963
|
+
variant: 'quiet',
|
|
964
|
+
attrs: { 'data-action': 'copy-judgment-reference' },
|
|
965
|
+
onClick: () => {
|
|
966
|
+
if (typeof onCopyReference === 'function') onCopyReference({ record, event, reason, preview, status });
|
|
967
|
+
else status.textContent = '参照情報を選択してコピーできます。';
|
|
968
|
+
},
|
|
969
|
+
}), status);
|
|
970
|
+
return wrap;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function appendNormalFeedbackEvents(doc, record, { onCopyReference } = {}) {
|
|
974
|
+
const events = Array.isArray(record.feedbackEvents) ? record.feedbackEvents : [];
|
|
975
|
+
const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-feedback-events' });
|
|
976
|
+
if (!events.length) {
|
|
977
|
+
wrap.append(makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '追記された結果・訂正・フィードバックはありません。' }));
|
|
978
|
+
wrap.append(appendNormalReferenceExport(doc, record, null, { onCopyReference }));
|
|
979
|
+
return wrap;
|
|
980
|
+
}
|
|
981
|
+
for (const event of events) {
|
|
982
|
+
const item = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-feedback-event' });
|
|
983
|
+
item.append(makeWorkspaceElement(doc, 'strong', { text: FEEDBACK_KIND_LABELS[event.kind] ?? event.kind }));
|
|
984
|
+
item.append(makeWorkspaceElement(doc, 'code', { text: `record_id=${event.recordId} event_id=${event.eventId}` }));
|
|
985
|
+
item.append(makeWorkspaceElement(doc, 'p', { text: normalFeedbackContentSummary(event.content) }));
|
|
986
|
+
item.append(appendNormalReferenceExport(doc, record, event, { onCopyReference }));
|
|
987
|
+
wrap.append(item);
|
|
988
|
+
}
|
|
989
|
+
return wrap;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function feedbackStatusText(draft) {
|
|
993
|
+
if (!draft) return '';
|
|
994
|
+
if (draft.phase === 'submitting') return '送信中です。保存状態はまだ未確認です。';
|
|
995
|
+
if (draft.phase === 'readback') return '正本を読み戻して保存状態を確認しています。';
|
|
996
|
+
if (draft.phase === 'saved') return '保存済み(正本で確認しました)。';
|
|
997
|
+
if (draft.phase === 'unavailable') return `利用できません。${draft.status || '正本に接続できないため、保存状態を確認できません。'}`;
|
|
998
|
+
if (draft.phase === 'unconfirmed') return `未確認。${draft.status || 'POST後の正本読戻しで一致を確認できませんでした。'}`;
|
|
999
|
+
if (draft.phase === 'error') return `送信できません。保存状態は未確認です。${draft.status ? ` ${draft.status}` : ''}`;
|
|
1000
|
+
return draft.status || '送信前です。';
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function appendNormalFeedbackComposer(doc, record, {
|
|
1004
|
+
detailReady = false,
|
|
1005
|
+
detailPhase = 'idle',
|
|
1006
|
+
draft,
|
|
1007
|
+
onKindChange,
|
|
1008
|
+
onSummaryChange,
|
|
1009
|
+
onOutcomeStatusChange,
|
|
1010
|
+
onSubmitFeedback,
|
|
1011
|
+
} = {}) {
|
|
1012
|
+
const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-feedback-composer' });
|
|
1013
|
+
if (!detailReady) {
|
|
1014
|
+
const message = detailPhase === 'loading'
|
|
1015
|
+
? '正本の詳細を確認しています。'
|
|
1016
|
+
: '正本の詳細を確認できないため、追記は利用できません。';
|
|
1017
|
+
wrap.append(makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: message }));
|
|
1018
|
+
return wrap;
|
|
1019
|
+
}
|
|
1020
|
+
const kindLabel = makeWorkspaceElement(doc, 'label', { text: '追記の種別' });
|
|
1021
|
+
const kind = makeWorkspaceElement(doc, 'select', { attrs: { 'aria-label': '追記の種別' } });
|
|
1022
|
+
for (const value of FEEDBACK_KINDS) {
|
|
1023
|
+
kind.append(makeWorkspaceElement(doc, 'option', {
|
|
1024
|
+
text: FEEDBACK_KIND_LABELS[value],
|
|
1025
|
+
attrs: { value },
|
|
1026
|
+
}));
|
|
1027
|
+
}
|
|
1028
|
+
kind.value = draft?.kind ?? 'feedback';
|
|
1029
|
+
kind.addEventListener('change', (event) => onKindChange?.(event?.target?.value ?? kind.value));
|
|
1030
|
+
const summaryLabel = makeWorkspaceElement(doc, 'label', { text: '内容(保存公開フィールド)' });
|
|
1031
|
+
const summary = makeWorkspaceElement(doc, 'textarea', {
|
|
1032
|
+
className: 'bb-jh-feedback-content',
|
|
1033
|
+
attrs: { rows: 3, 'aria-label': '追記の内容', placeholder: '結果・訂正・フィードバックを記入' },
|
|
1034
|
+
});
|
|
1035
|
+
summary.value = draft?.summary ?? '';
|
|
1036
|
+
summary.addEventListener('input', (event) => onSummaryChange?.(event?.target?.value ?? summary.value));
|
|
1037
|
+
const outcomeLabel = makeWorkspaceElement(doc, 'label', { text: '結果の確認状態(明示する場合)' });
|
|
1038
|
+
const outcomeStatus = makeWorkspaceElement(doc, 'select', { attrs: { 'aria-label': '結果の確認状態' } });
|
|
1039
|
+
outcomeStatus.append(makeWorkspaceElement(doc, 'option', { text: '記録しない', attrs: { value: '' } }));
|
|
1040
|
+
for (const value of ['unknown', 'unconfirmed', 'confirmed']) {
|
|
1041
|
+
const label = { unknown: '未確認', unconfirmed: '結果未確認', confirmed: '結果確認済み' }[value];
|
|
1042
|
+
outcomeStatus.append(makeWorkspaceElement(doc, 'option', { text: label, attrs: { value } }));
|
|
1043
|
+
}
|
|
1044
|
+
outcomeStatus.value = draft?.outcomeStatus ?? '';
|
|
1045
|
+
outcomeStatus.addEventListener('change', (event) => onOutcomeStatusChange?.(event?.target?.value ?? outcomeStatus.value));
|
|
1046
|
+
const eventId = makeWorkspaceElement(doc, 'code', { text: `event_id=${draft?.eventId ?? '未発行'}` });
|
|
1047
|
+
const phase = draft?.phase ?? 'idle';
|
|
1048
|
+
const busy = phase === 'submitting' || phase === 'readback';
|
|
1049
|
+
const button = workspaceButton(doc, {
|
|
1050
|
+
text: phase === 'saved' ? '保存済み' : '結果・訂正・フィードバックを追記',
|
|
1051
|
+
variant: 'primary',
|
|
1052
|
+
disabled: busy || phase === 'saved',
|
|
1053
|
+
attrs: { 'data-action': 'submit-judgment-feedback' },
|
|
1054
|
+
onClick: () => onSubmitFeedback?.({ kind, summary, outcomeStatus, eventId: draft?.eventId }),
|
|
1055
|
+
});
|
|
1056
|
+
const status = makeWorkspaceElement(doc, 'p', {
|
|
1057
|
+
className: `bb-jh-feedback-status is-${phase}`,
|
|
1058
|
+
text: feedbackStatusText(draft),
|
|
1059
|
+
attrs: { role: phase === 'error' || phase === 'unavailable' ? 'alert' : 'status' },
|
|
1060
|
+
});
|
|
1061
|
+
wrap.append(kindLabel, kind, summaryLabel, summary, outcomeLabel, outcomeStatus, eventId, button, status);
|
|
1062
|
+
return wrap;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function renderNormalRail(doc, rail, row, detail, onClose, options = {}) {
|
|
1066
|
+
if (!rail) return;
|
|
1067
|
+
rail.replaceChildren();
|
|
1068
|
+
const record = detail ?? row;
|
|
1069
|
+
rail.append(workspaceRailHead(doc, {
|
|
1070
|
+
kicker: `${NORMAL_ENTRYPOINT_LABELS[record.entrypoint] ?? '入口不明'}の判断`,
|
|
1071
|
+
title: text(record.judgment?.summary) || '判断内容は未記録',
|
|
1072
|
+
sub: formatDate(record.recordedAt),
|
|
1073
|
+
}));
|
|
1074
|
+
if (typeof onClose === 'function') {
|
|
1075
|
+
rail.append(workspaceButton(doc, {
|
|
1076
|
+
text: '詳細を閉じる',
|
|
1077
|
+
variant: 'quiet',
|
|
1078
|
+
onClick: onClose,
|
|
1079
|
+
attrs: { 'aria-label': '判断の詳細を閉じる' },
|
|
1080
|
+
}));
|
|
1081
|
+
}
|
|
1082
|
+
rail.append(workspaceRailBlock(doc, {
|
|
1083
|
+
title: '使った参照(当時の記録)',
|
|
1084
|
+
content: appendReferenceList(doc, record.selectedReferences),
|
|
1085
|
+
}));
|
|
1086
|
+
rail.append(workspaceRailBlock(doc, {
|
|
1087
|
+
title: '判断の理由',
|
|
1088
|
+
content: appendTextParts(doc, [text(record.judgment?.reason) || '理由は記録されていません。']),
|
|
1089
|
+
}));
|
|
1090
|
+
rail.append(workspaceRailBlock(doc, {
|
|
1091
|
+
title: '保存された案',
|
|
1092
|
+
content: appendNormalAlternatives(doc, record.alternatives),
|
|
1093
|
+
}));
|
|
1094
|
+
rail.append(workspaceRailBlock(doc, {
|
|
1095
|
+
title: '実行と結果',
|
|
1096
|
+
content: appendNormalExecution(doc, record.execution),
|
|
1097
|
+
}));
|
|
1098
|
+
rail.append(workspaceRailBlock(doc, {
|
|
1099
|
+
title: '結果・訂正・フィードバック',
|
|
1100
|
+
content: appendNormalFeedbackEvents(doc, record, { onCopyReference: options.onCopyReference }),
|
|
1101
|
+
}));
|
|
1102
|
+
rail.append(workspaceRailBlock(doc, {
|
|
1103
|
+
title: '次の判断に渡す参照',
|
|
1104
|
+
content: appendNormalFeedbackComposer(doc, record, options),
|
|
1105
|
+
}));
|
|
1106
|
+
const metadata = workspaceDefinition(doc, [
|
|
1107
|
+
['記録ID', record.recordId],
|
|
1108
|
+
['プロジェクト', record.projectCode],
|
|
1109
|
+
['Turn', record.turnRef],
|
|
1110
|
+
]);
|
|
1111
|
+
const missing = appendNormalMissingFields(doc, record.missingFields);
|
|
1112
|
+
if (missing) metadata.append(missing);
|
|
1113
|
+
rail.append(workspaceRailBlock(doc, { title: '記録の範囲', content: metadata }));
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function renderRail(doc, rail, row, detail, onClose, options = {}) {
|
|
482
1117
|
if (!rail) return;
|
|
483
1118
|
rail.replaceChildren();
|
|
484
1119
|
if (!row) {
|
|
@@ -489,6 +1124,10 @@ function renderRail(doc, rail, row, onClose) {
|
|
|
489
1124
|
}));
|
|
490
1125
|
return;
|
|
491
1126
|
}
|
|
1127
|
+
if (row.mode === 'normal') {
|
|
1128
|
+
renderNormalRail(doc, rail, row, detail, onClose, options);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
492
1131
|
const proof = row.proof;
|
|
493
1132
|
rail.append(workspaceRailHead(doc, {
|
|
494
1133
|
kicker: row.delegated ? 'Brainbaseの判断' : '判断履歴',
|
|
@@ -531,9 +1170,16 @@ function statusNotice(doc, phase, error, home, onRetry) {
|
|
|
531
1170
|
tone: 'danger',
|
|
532
1171
|
role: 'alert',
|
|
533
1172
|
});
|
|
1173
|
+
if (home?.status === 'error') return workspaceNotice(doc, {
|
|
1174
|
+
label: '取得に失敗',
|
|
1175
|
+
text: ['判断の記録を確認できません。データが0件とは確認できていません。', workspaceButton(doc, { text: '再読み込み', variant: 'quiet', onClick: onRetry })],
|
|
1176
|
+
tone: 'danger',
|
|
1177
|
+
role: 'alert',
|
|
1178
|
+
});
|
|
1179
|
+
const sourceSummary = normalSourcesSummary(home);
|
|
534
1180
|
if (home?.status === 'unavailable') return workspaceNotice(doc, {
|
|
535
1181
|
label: '未接続',
|
|
536
|
-
text: ['
|
|
1182
|
+
text: [`保存された判断に接続できません${home.coverage?.storage ? `(${normalStorageLabel(home.coverage.storage)})` : ''}。データが0件とは確認できていません。`, sourceSummary ? `取得元: ${sourceSummary}` : null, workspaceButton(doc, { text: '再読み込み', variant: 'quiet', onClick: onRetry })],
|
|
537
1183
|
tone: 'warning',
|
|
538
1184
|
role: 'alert',
|
|
539
1185
|
});
|
|
@@ -545,7 +1191,7 @@ function statusNotice(doc, phase, error, home, onRetry) {
|
|
|
545
1191
|
});
|
|
546
1192
|
if (home?.status === 'partial') return workspaceNotice(doc, {
|
|
547
1193
|
label: '取得できた範囲',
|
|
548
|
-
text: [`読み取れた判断だけを表示しています。${home.coverage?.rejected ?
|
|
1194
|
+
text: [`読み取れた判断だけを表示しています。${home.coverage?.storage ? `(${normalStorageLabel(home.coverage.storage)})` : ''}${home.coverage?.reason ? ` ${normalReasonLabel(home.coverage.reason)}` : ''}${home.coverage?.rejected ? ` 読み取れなかった記録は${home.coverage.rejected}件あります。` : ''}`, sourceSummary ? `取得元: ${sourceSummary}` : null, workspaceButton(doc, { text: '再読み込み', variant: 'quiet', onClick: onRetry })],
|
|
549
1195
|
tone: 'warning',
|
|
550
1196
|
role: 'status',
|
|
551
1197
|
});
|
|
@@ -577,12 +1223,108 @@ function createSearch(doc, value, onInput) {
|
|
|
577
1223
|
return label;
|
|
578
1224
|
}
|
|
579
1225
|
|
|
1226
|
+
function normalStorageLabel(storage) {
|
|
1227
|
+
if (storage === 'local') return '端末';
|
|
1228
|
+
if (storage === 'server') return 'サーバー';
|
|
1229
|
+
return storage || '接続先不明';
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function normalReasonLabel(reason) {
|
|
1233
|
+
const labels = {
|
|
1234
|
+
unconnected: '未接続',
|
|
1235
|
+
mana_not_connected: '未接続(Mana)',
|
|
1236
|
+
company_os_native_source_not_connected: 'Company OSの判断実行履歴が未接続',
|
|
1237
|
+
journal_unavailable: '判断記録がありません',
|
|
1238
|
+
journal_unreadable: '判断記録を読み取れません',
|
|
1239
|
+
};
|
|
1240
|
+
return labels[reason] ?? reason;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function normalSourceStatusLabel(status) {
|
|
1244
|
+
const labels = {
|
|
1245
|
+
available: '接続済み',
|
|
1246
|
+
partial: '一部取得',
|
|
1247
|
+
unavailable: '未接続',
|
|
1248
|
+
};
|
|
1249
|
+
return labels[status] ?? '未確認';
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function normalSourcesSummary(home) {
|
|
1253
|
+
if (home?.mode !== 'normal' || !Array.isArray(home.coverage?.sources) || home.coverage.sources.length === 0) return '';
|
|
1254
|
+
return home.coverage.sources.map((source) => {
|
|
1255
|
+
const entrypoint = NORMAL_ENTRYPOINT_LABELS[normalEntrypoint(source.entrypoint)] ?? '入口不明';
|
|
1256
|
+
const reason = source.reason ? `(${normalReasonLabel(source.reason)})` : '';
|
|
1257
|
+
return `${entrypoint}: ${normalSourceStatusLabel(source.status)}${reason}`;
|
|
1258
|
+
}).join('、');
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function createFilterSelect(doc, { label, value, options, onChange, className = '' }) {
|
|
1262
|
+
const wrap = makeWorkspaceElement(doc, 'label', { className: `bb-jh-filter${className ? ` ${className}` : ''}` });
|
|
1263
|
+
wrap.append(makeWorkspaceElement(doc, 'span', { text: label }));
|
|
1264
|
+
const select = makeWorkspaceElement(doc, 'select', { attrs: { 'aria-label': label } });
|
|
1265
|
+
const current = value ?? '';
|
|
1266
|
+
const choices = [{ value: '', label: 'すべて' }, ...options];
|
|
1267
|
+
for (const option of choices) {
|
|
1268
|
+
const optionElement = makeWorkspaceElement(doc, 'option', { text: option.label, attrs: { value: option.value, selected: option.value === current ? 'selected' : undefined } });
|
|
1269
|
+
optionElement.value = option.value;
|
|
1270
|
+
select.append(optionElement);
|
|
1271
|
+
}
|
|
1272
|
+
select.value = current;
|
|
1273
|
+
select.addEventListener('change', (event) => onChange(event?.target?.value ?? select.value ?? ''));
|
|
1274
|
+
wrap.append(select);
|
|
1275
|
+
return wrap;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function normalFilterControls(doc, home, state, onChange) {
|
|
1279
|
+
const filters = home?.filters ?? {};
|
|
1280
|
+
const projects = [...(filters.projects ?? [])];
|
|
1281
|
+
const knownProjects = (home.records ?? []).map((record) => record.projectCode).filter(Boolean);
|
|
1282
|
+
for (const project of knownProjects) {
|
|
1283
|
+
if (!projects.some((option) => option.value === project)) projects.push({ value: project, label: project });
|
|
1284
|
+
}
|
|
1285
|
+
const entrypoints = [...(filters.entrypoints ?? [])];
|
|
1286
|
+
const knownEntrypoints = (home.records ?? []).map((record) => record.entrypoint).filter(Boolean);
|
|
1287
|
+
for (const entrypoint of [...NORMAL_ENTRYPOINTS, ...knownEntrypoints]) {
|
|
1288
|
+
if (!entrypoints.some((option) => option.value === entrypoint)) {
|
|
1289
|
+
entrypoints.push({ value: entrypoint, label: NORMAL_ENTRYPOINT_LABELS[entrypoint] ?? entrypoint });
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-normal-filters', attrs: { 'aria-label': '絞り込み' } });
|
|
1293
|
+
wrap.append(createFilterSelect(doc, {
|
|
1294
|
+
label: 'プロジェクト',
|
|
1295
|
+
value: state.project,
|
|
1296
|
+
options: projects,
|
|
1297
|
+
onChange: (project) => onChange({ project: project || null }),
|
|
1298
|
+
}));
|
|
1299
|
+
wrap.append(createFilterSelect(doc, {
|
|
1300
|
+
label: '入口',
|
|
1301
|
+
value: state.entrypoint,
|
|
1302
|
+
options: entrypoints,
|
|
1303
|
+
onChange: (entrypoint) => onChange({ entrypoint: entrypoint || null }),
|
|
1304
|
+
}));
|
|
1305
|
+
return wrap;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function paginationControls(doc, aggregate, onNext) {
|
|
1309
|
+
if (aggregate?.mode !== 'normal') return null;
|
|
1310
|
+
const pagination = aggregate?.pagination;
|
|
1311
|
+
const total = aggregate?.totalCount;
|
|
1312
|
+
if (!pagination?.hasNext && total === null) return null;
|
|
1313
|
+
const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-pagination' });
|
|
1314
|
+
const count = total === null ? '同じ範囲の件数は未確認' : `同じ範囲の件数: ${total}件`;
|
|
1315
|
+
wrap.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-pagination-count', text: count }));
|
|
1316
|
+
if (pagination?.hasNext) {
|
|
1317
|
+
wrap.append(workspaceButton(doc, { text: '次のページ', variant: 'quiet', onClick: onNext, attrs: { 'data-action': 'next-page' } }));
|
|
1318
|
+
}
|
|
1319
|
+
return wrap;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
580
1322
|
function makeRows(doc, aggregate, selectedKey, onSelect) {
|
|
581
1323
|
return aggregate.rows.map((row) => ({
|
|
582
1324
|
key: row.key,
|
|
583
|
-
label: `${formatDate(row.recordedAt)} ${text(row.proof?.decision?.summary) || '判断履歴'}`,
|
|
1325
|
+
label: `${formatDate(row.recordedAt)} ${row.mode === 'normal' ? text(row.judgment?.summary) || '判断履歴' : text(row.proof?.decision?.summary) || '判断履歴'}`,
|
|
584
1326
|
selected: row.key === selectedKey,
|
|
585
|
-
className: `bb-jh-row${row.delegated ? ' is-delegated' : ' is-returned'}`,
|
|
1327
|
+
className: `bb-jh-row${row.mode === 'normal' ? ' is-normal' : row.delegated ? ' is-delegated' : ' is-returned'}`,
|
|
586
1328
|
onSelect,
|
|
587
1329
|
cells: [
|
|
588
1330
|
makeWorkspaceElement(doc, 'span', { className: 'bb-jh-date', text: formatDate(row.recordedAt) }),
|
|
@@ -592,14 +1334,14 @@ function makeRows(doc, aggregate, selectedKey, onSelect) {
|
|
|
592
1334
|
}));
|
|
593
1335
|
}
|
|
594
1336
|
|
|
595
|
-
/** Create the shared
|
|
1337
|
+
/** Create the shared judgment history controller and canonical feedback adapter. */
|
|
596
1338
|
export function createJudgmentHistoryUI({
|
|
597
1339
|
root,
|
|
598
1340
|
rail = null,
|
|
599
1341
|
document: explicitDocument,
|
|
600
1342
|
fetcher,
|
|
601
1343
|
token,
|
|
602
|
-
basePath = '/api/
|
|
1344
|
+
basePath = '/api/judgment-history',
|
|
603
1345
|
page,
|
|
604
1346
|
autoLoad = true,
|
|
605
1347
|
pageHeader = true,
|
|
@@ -611,6 +1353,7 @@ export function createJudgmentHistoryUI({
|
|
|
611
1353
|
? fetcher
|
|
612
1354
|
: typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
|
|
613
1355
|
const normalizedBasePath = String(basePath).replace(/\/+$/u, '');
|
|
1356
|
+
const legacyBasePath = normalizedBasePath === '/api/judgment-history' ? '/api/value-proofs' : null;
|
|
614
1357
|
const readNow = () => (typeof now === 'function' ? now() : now);
|
|
615
1358
|
const state = {
|
|
616
1359
|
phase: autoLoad ? 'loading' : 'idle',
|
|
@@ -619,9 +1362,19 @@ export function createJudgmentHistoryUI({
|
|
|
619
1362
|
aggregate: null,
|
|
620
1363
|
period: PERIODS.past30days,
|
|
621
1364
|
query: '',
|
|
1365
|
+
project: null,
|
|
1366
|
+
entrypoint: null,
|
|
1367
|
+
cursor: null,
|
|
1368
|
+
limit: 50,
|
|
622
1369
|
selectedKey: null,
|
|
1370
|
+
details: new Map(),
|
|
1371
|
+
detailPhase: 'idle',
|
|
1372
|
+
detailError: null,
|
|
1373
|
+
activeBasePath: normalizedBasePath,
|
|
1374
|
+
feedbackDrafts: new Map(),
|
|
623
1375
|
disposed: false,
|
|
624
1376
|
generation: 0,
|
|
1377
|
+
detailGeneration: 0,
|
|
625
1378
|
focusSearch: false,
|
|
626
1379
|
lastFocusedKey: null,
|
|
627
1380
|
pendingDecisionId: null,
|
|
@@ -631,6 +1384,8 @@ export function createJudgmentHistoryUI({
|
|
|
631
1384
|
if (!state.selectedKey || state.disposed) return false;
|
|
632
1385
|
state.lastFocusedKey = state.selectedKey;
|
|
633
1386
|
state.selectedKey = null;
|
|
1387
|
+
state.detailPhase = 'idle';
|
|
1388
|
+
state.detailError = null;
|
|
634
1389
|
render();
|
|
635
1390
|
return true;
|
|
636
1391
|
}
|
|
@@ -650,6 +1405,229 @@ export function createJudgmentHistoryUI({
|
|
|
650
1405
|
};
|
|
651
1406
|
}
|
|
652
1407
|
|
|
1408
|
+
function newFeedbackEventId() {
|
|
1409
|
+
try {
|
|
1410
|
+
if (typeof globalThis.crypto?.randomUUID === 'function') return globalThis.crypto.randomUUID();
|
|
1411
|
+
} catch {
|
|
1412
|
+
// The fallback below is sufficient for a retry token when Web Crypto is unavailable.
|
|
1413
|
+
}
|
|
1414
|
+
return `judgment-history-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function feedbackDraft(recordId) {
|
|
1418
|
+
if (!recordId) return null;
|
|
1419
|
+
let draft = state.feedbackDrafts.get(recordId);
|
|
1420
|
+
if (!draft) {
|
|
1421
|
+
draft = {
|
|
1422
|
+
recordId,
|
|
1423
|
+
eventId: newFeedbackEventId(),
|
|
1424
|
+
kind: 'feedback',
|
|
1425
|
+
summary: '',
|
|
1426
|
+
outcomeStatus: '',
|
|
1427
|
+
phase: 'idle',
|
|
1428
|
+
status: '',
|
|
1429
|
+
};
|
|
1430
|
+
state.feedbackDrafts.set(recordId, draft);
|
|
1431
|
+
}
|
|
1432
|
+
return draft;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function updateFeedbackDraft(recordId, changes) {
|
|
1436
|
+
const current = feedbackDraft(recordId);
|
|
1437
|
+
if (!current || !isRecord(changes)) return current;
|
|
1438
|
+
const next = { ...current, ...changes };
|
|
1439
|
+
state.feedbackDrafts.set(recordId, next);
|
|
1440
|
+
return next;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function feedbackReadbackMatches(detail, expected) {
|
|
1444
|
+
if (!detail || !expected) return false;
|
|
1445
|
+
const events = Array.isArray(detail.feedbackEvents) ? detail.feedbackEvents : [];
|
|
1446
|
+
return events.some((event) => event.recordId === expected.recordId
|
|
1447
|
+
&& event.eventId === expected.eventId
|
|
1448
|
+
&& event.kind === expected.kind
|
|
1449
|
+
&& canonicalJson(event.content) === canonicalJson(expected.content));
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function responseFeedbackReadback(payload, expected) {
|
|
1453
|
+
if (!isRecord(payload) || !expected) return false;
|
|
1454
|
+
const candidate = payload.event
|
|
1455
|
+
?? payload.feedback_event
|
|
1456
|
+
?? (isRecord(payload.feedback) ? payload.feedback : null)
|
|
1457
|
+
?? (text(payload.event_id) ? payload : null);
|
|
1458
|
+
const event = normalizeFeedbackEvent(candidate, expected.recordId);
|
|
1459
|
+
if (!event) return false;
|
|
1460
|
+
const storageStatus = text(payload.storage_status ?? payload.storage?.status ?? payload.save_status).toLowerCase();
|
|
1461
|
+
const saved = payload.saved === true || event.saved === true || ['saved', 'stored', 'idempotent', 'confirmed'].includes(storageStatus)
|
|
1462
|
+
|| ['saved', 'stored', 'idempotent', 'confirmed'].includes(event.storageStatus)
|
|
1463
|
+
|| text(payload.status).toLowerCase() === 'saved';
|
|
1464
|
+
return saved
|
|
1465
|
+
&& event.recordId === expected.recordId
|
|
1466
|
+
&& event.eventId === expected.eventId
|
|
1467
|
+
&& event.kind === expected.kind
|
|
1468
|
+
&& canonicalJson(event.content) === canonicalJson(expected.content);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
async function feedbackReference(record, event) {
|
|
1472
|
+
if (!record || !event) return null;
|
|
1473
|
+
try {
|
|
1474
|
+
const subtle = globalThis.crypto?.subtle;
|
|
1475
|
+
if (!subtle || typeof subtle.digest !== 'function' || typeof TextEncoder !== 'function') return null;
|
|
1476
|
+
const bytes = await subtle.digest('SHA-256', new TextEncoder().encode(event.eventId));
|
|
1477
|
+
const hash = [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
1478
|
+
return `judgment-history-feedback:${record.recordId}:${hash}`;
|
|
1479
|
+
} catch {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
async function copyReference({ record, event, reason, preview, status } = {}) {
|
|
1485
|
+
if (!record || !preview || !status) return false;
|
|
1486
|
+
const applicabilityReason = reason?.value ?? '';
|
|
1487
|
+
preview.textContent = normalReferencePayloadText(record, event, applicabilityReason);
|
|
1488
|
+
const eventRef = await feedbackReference(record, event);
|
|
1489
|
+
const serialized = normalReferencePayloadText(record, event, applicabilityReason, eventRef);
|
|
1490
|
+
preview.textContent = serialized;
|
|
1491
|
+
try {
|
|
1492
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
1493
|
+
if (!clipboard || typeof clipboard.writeText !== 'function') throw new Error('clipboard_unavailable');
|
|
1494
|
+
await clipboard.writeText(serialized);
|
|
1495
|
+
status.textContent = '参照情報をコピーしました。';
|
|
1496
|
+
return true;
|
|
1497
|
+
} catch {
|
|
1498
|
+
status.textContent = '参照情報を表示しました。選択してコピーできます。';
|
|
1499
|
+
return false;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
async function submitFeedback(row, controls) {
|
|
1504
|
+
if (!row || row.mode !== 'normal' || !controls || !request || state.disposed) return false;
|
|
1505
|
+
const current = feedbackDraft(row.recordId);
|
|
1506
|
+
const kind = feedbackKind(controls.kind?.value ?? current?.kind) ?? 'feedback';
|
|
1507
|
+
const summary = text(controls.summary?.value ?? current?.summary);
|
|
1508
|
+
const outcomeStatus = text(controls.outcomeStatus?.value ?? current?.outcomeStatus).toLowerCase();
|
|
1509
|
+
if (!summary) {
|
|
1510
|
+
updateFeedbackDraft(row.recordId, { kind, summary, outcomeStatus: '', phase: 'error', status: '内容を入力してください。' });
|
|
1511
|
+
render();
|
|
1512
|
+
return false;
|
|
1513
|
+
}
|
|
1514
|
+
const acceptedOutcomeStatuses = new Set(['unknown', 'unconfirmed', 'confirmed']);
|
|
1515
|
+
const content = { summary };
|
|
1516
|
+
if (acceptedOutcomeStatuses.has(outcomeStatus)) content.outcome_status = outcomeStatus;
|
|
1517
|
+
const displayedEventId = text(controls.eventId?.textContent);
|
|
1518
|
+
const eventId = displayedEventId.startsWith('event_id=')
|
|
1519
|
+
? displayedEventId.slice('event_id='.length)
|
|
1520
|
+
: displayedEventId || current?.eventId || newFeedbackEventId();
|
|
1521
|
+
const expected = { recordId: row.recordId, eventId, kind, content };
|
|
1522
|
+
updateFeedbackDraft(row.recordId, { eventId, kind, summary, outcomeStatus, phase: 'submitting', status: '' });
|
|
1523
|
+
render();
|
|
1524
|
+
try {
|
|
1525
|
+
const response = await request(`${state.activeBasePath}/feedback`, {
|
|
1526
|
+
method: 'POST',
|
|
1527
|
+
headers: {
|
|
1528
|
+
'content-type': 'application/json',
|
|
1529
|
+
...(token ? { 'x-brainbase-review-token': token } : {}),
|
|
1530
|
+
},
|
|
1531
|
+
body: JSON.stringify({ record_id: row.recordId, event_id: eventId, kind, content }),
|
|
1532
|
+
});
|
|
1533
|
+
if (!response || response.ok !== true) {
|
|
1534
|
+
const statusCode = response?.status ?? 0;
|
|
1535
|
+
const unavailable = [401, 403, 404, 405, 501, 503].includes(statusCode);
|
|
1536
|
+
updateFeedbackDraft(row.recordId, {
|
|
1537
|
+
phase: unavailable ? 'unavailable' : 'unconfirmed',
|
|
1538
|
+
status: unavailable ? `正本へ接続できません(HTTP ${statusCode})。` : `受付応答はHTTP ${statusCode}でした。`,
|
|
1539
|
+
});
|
|
1540
|
+
render();
|
|
1541
|
+
return false;
|
|
1542
|
+
}
|
|
1543
|
+
let responsePayload = null;
|
|
1544
|
+
if (typeof response.json === 'function') {
|
|
1545
|
+
try { responsePayload = await response.json(); } catch { responsePayload = null; }
|
|
1546
|
+
}
|
|
1547
|
+
updateFeedbackDraft(row.recordId, { phase: 'readback', status: '' });
|
|
1548
|
+
render();
|
|
1549
|
+
const detail = await loadDetail(row);
|
|
1550
|
+
const detailReadback = feedbackReadbackMatches(detail, expected);
|
|
1551
|
+
const responseReadback = responseFeedbackReadback(responsePayload, expected);
|
|
1552
|
+
if (detailReadback || responseReadback) {
|
|
1553
|
+
updateFeedbackDraft(row.recordId, { phase: 'saved', status: '' });
|
|
1554
|
+
} else if (state.detailPhase === 'error') {
|
|
1555
|
+
updateFeedbackDraft(row.recordId, { phase: 'unavailable', status: '正本を読み戻せませんでした。' });
|
|
1556
|
+
} else {
|
|
1557
|
+
updateFeedbackDraft(row.recordId, { phase: 'unconfirmed', status: '同じevent_idの正本記録を確認できませんでした。' });
|
|
1558
|
+
}
|
|
1559
|
+
render();
|
|
1560
|
+
return detailReadback || responseReadback;
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
updateFeedbackDraft(row.recordId, {
|
|
1563
|
+
phase: 'unavailable',
|
|
1564
|
+
status: `正本へ接続できません。${error instanceof Error ? error.message : String(error)}`,
|
|
1565
|
+
});
|
|
1566
|
+
render();
|
|
1567
|
+
return false;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function historyPath(path, cursor = null) {
|
|
1572
|
+
const params = new URLSearchParams();
|
|
1573
|
+
params.set('period', state.period);
|
|
1574
|
+
if (state.project) params.set('project', state.project);
|
|
1575
|
+
if (state.entrypoint) params.set('entrypoint', state.entrypoint);
|
|
1576
|
+
params.set('limit', String(state.limit));
|
|
1577
|
+
if (cursor) params.set('cursor', cursor);
|
|
1578
|
+
return `${path}/home?${params.toString()}`;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function normalMode() {
|
|
1582
|
+
return state.home?.mode === 'normal' || state.aggregate?.mode === 'normal';
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function handleNormalFilterChange(changes) {
|
|
1586
|
+
if (!normalMode()) return;
|
|
1587
|
+
if (Object.prototype.hasOwnProperty.call(changes, 'project')) state.project = changes.project;
|
|
1588
|
+
if (Object.prototype.hasOwnProperty.call(changes, 'entrypoint')) state.entrypoint = changes.entrypoint;
|
|
1589
|
+
state.cursor = null;
|
|
1590
|
+
state.selectedKey = null;
|
|
1591
|
+
state.details = new Map();
|
|
1592
|
+
void load({ append: false });
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
async function loadDetail(row) {
|
|
1596
|
+
if (!row || row.mode !== 'normal' || !request || state.disposed) return null;
|
|
1597
|
+
const generation = ++state.detailGeneration;
|
|
1598
|
+
state.detailPhase = 'loading';
|
|
1599
|
+
state.detailError = null;
|
|
1600
|
+
render();
|
|
1601
|
+
try {
|
|
1602
|
+
const response = await request(`${state.activeBasePath}/records/${encodeURIComponent(row.recordId)}`, {
|
|
1603
|
+
method: 'GET',
|
|
1604
|
+
headers: token ? { 'x-brainbase-review-token': token } : undefined,
|
|
1605
|
+
});
|
|
1606
|
+
if (generation !== state.detailGeneration || state.disposed) return null;
|
|
1607
|
+
if (!response || response.ok !== true) throw new Error(`http_${response?.status ?? 0}`);
|
|
1608
|
+
const payload = await response.json();
|
|
1609
|
+
if (generation !== state.detailGeneration || state.disposed) return null;
|
|
1610
|
+
let rawRecord = isRecord(payload?.record) ? { ...payload.record } : isRecord(payload) && text(payload.record_id) ? payload : null;
|
|
1611
|
+
if (rawRecord && isRecord(payload)) {
|
|
1612
|
+
for (const key of ['feedback_events', 'feedback', 'events', 'appended_events', 'event', 'feedback_event']) {
|
|
1613
|
+
if (rawRecord[key] === undefined && payload[key] !== undefined) rawRecord[key] = payload[key];
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
const detail = normalizeNormalRecord(rawRecord);
|
|
1617
|
+
if (!detail) throw new Error('record_response_invalid');
|
|
1618
|
+
state.details.set(row.recordId, detail);
|
|
1619
|
+
state.detailPhase = 'ready';
|
|
1620
|
+
render();
|
|
1621
|
+
return detail;
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
if (generation !== state.detailGeneration || state.disposed) return null;
|
|
1624
|
+
state.detailPhase = 'error';
|
|
1625
|
+
state.detailError = error instanceof Error ? error.message : String(error);
|
|
1626
|
+
render();
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
653
1631
|
function render() {
|
|
654
1632
|
if (state.disposed) return;
|
|
655
1633
|
const previousSearchInput = findFirst(root, (node) => node.tagName === 'INPUT');
|
|
@@ -672,19 +1650,24 @@ export function createJudgmentHistoryUI({
|
|
|
672
1650
|
}
|
|
673
1651
|
const notice = statusNotice(doc, state.phase, state.error, state.home, () => void load());
|
|
674
1652
|
if (notice) wrapper.append(notice);
|
|
675
|
-
if (state.phase === 'loading' || state.phase === 'error' || state.home?.status === 'unavailable' || state.home?.status === 'invalid') {
|
|
1653
|
+
if (state.phase === 'loading' || state.phase === 'error' || state.home?.status === 'unavailable' || state.home?.status === 'error' || state.home?.status === 'invalid') {
|
|
676
1654
|
wrapper.append(workspaceDetailEmpty(doc, { mark: '…', title: state.phase === 'loading' ? '判断履歴を読み込んでいます' : '判断履歴を確認できません', text: '再試行すると最新の状態を確認します。' }));
|
|
677
1655
|
root.replaceChildren(wrapper);
|
|
678
|
-
renderRail(doc, rail, null);
|
|
1656
|
+
renderRail(doc, rail, null, null);
|
|
679
1657
|
return;
|
|
680
1658
|
}
|
|
681
1659
|
const controls = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-controls' });
|
|
682
1660
|
controls.append(periodButtons(doc, state.period, (period) => {
|
|
683
1661
|
state.period = period;
|
|
684
1662
|
state.selectedKey = null;
|
|
685
|
-
state.
|
|
686
|
-
|
|
1663
|
+
state.cursor = null;
|
|
1664
|
+
if (normalMode()) void load({ append: false });
|
|
1665
|
+
else {
|
|
1666
|
+
state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
|
|
1667
|
+
render();
|
|
1668
|
+
}
|
|
687
1669
|
}));
|
|
1670
|
+
if (normalMode()) controls.append(normalFilterControls(doc, state.home, state, handleNormalFilterChange));
|
|
688
1671
|
controls.append(createSearch(doc, state.query, (query) => {
|
|
689
1672
|
state.query = String(query);
|
|
690
1673
|
state.focusSearch = true;
|
|
@@ -699,20 +1682,26 @@ export function createJudgmentHistoryUI({
|
|
|
699
1682
|
attrs: { 'aria-label': '判断履歴を再読み込み', 'data-action': 'reload' },
|
|
700
1683
|
}));
|
|
701
1684
|
wrapper.append(controls);
|
|
1685
|
+
const normalSourceSummary = normalSourcesSummary(state.home);
|
|
702
1686
|
wrapper.append(makeWorkspaceElement(doc, 'p', {
|
|
703
1687
|
className: 'bb-jh-reference-note',
|
|
704
|
-
text:
|
|
1688
|
+
text: normalMode()
|
|
1689
|
+
? `保存された判断の理由、当時の参照、案、実行と結果を表示しています。参照の本文は現在の権限で確認できる場合だけ開きます。${normalSourceSummary ? ` 取得元: ${normalSourceSummary}` : ''}`
|
|
1690
|
+
: '判断に残った参照だけを表示しています。実行結果の証拠は参照件数に含めません。',
|
|
705
1691
|
}));
|
|
706
|
-
if (state.home?.status === 'partial') wrapper.append(statusNotice(doc, 'ready', null, state.home, () => void load()));
|
|
707
1692
|
const stats = aggregate.stats ?? {};
|
|
708
1693
|
const metricCount = (value, suffix) => value === null || value === undefined ? '未確認' : `${value}${suffix}`;
|
|
709
|
-
wrapper.append(workspaceMetrics(doc, [
|
|
1694
|
+
wrapper.append(workspaceMetrics(doc, normalMode() ? [
|
|
1695
|
+
{ label: '判断件数', value: metricCount(stats.judgments, '件'), note: 'この期間・絞り込みの履歴' },
|
|
1696
|
+
{ label: '使った参照', value: metricCount(stats.references, '件'), note: '当時の記録にある一意の参照' },
|
|
1697
|
+
{ label: '確認済みの結果', value: metricCount(stats.confirmedOutcomes, '件'), note: '成果が確認済みと記録された判断' },
|
|
1698
|
+
] : [
|
|
710
1699
|
{ label: 'Brainbaseが代わりに判断した件数', value: metricCount(stats.judgments, '件'), note: 'この期間の履歴' },
|
|
711
1700
|
{ label: 'そこで使った参照', value: metricCount(stats.references, '件'), note: '代理判断に含まれる一意の参照' },
|
|
712
1701
|
{ label: '判断回数にすると', value: metricCount(stats.equivalent, '回分相当'), note: '代理判断1件を1回分として表示' },
|
|
713
1702
|
], { ariaLabel: '判断履歴の集計' }));
|
|
714
1703
|
const emptyText = state.home?.status === 'partial'
|
|
715
|
-
? '
|
|
1704
|
+
? 'この期間に取得できた判断履歴はありません。未接続・一部取得のため0件とは確認できません。'
|
|
716
1705
|
: 'この期間の判断履歴はありません。';
|
|
717
1706
|
wrapper.append(workspaceLedger(doc, {
|
|
718
1707
|
className: 'bb-jh-ledger',
|
|
@@ -721,10 +1710,21 @@ export function createJudgmentHistoryUI({
|
|
|
721
1710
|
rows: makeRows(doc, aggregate, state.selectedKey, (key) => {
|
|
722
1711
|
state.lastFocusedKey = key;
|
|
723
1712
|
state.selectedKey = key;
|
|
1713
|
+
state.detailPhase = 'idle';
|
|
1714
|
+
state.detailError = null;
|
|
724
1715
|
render();
|
|
1716
|
+
const selectedRow = aggregate.rows.find((row) => row.key === key);
|
|
1717
|
+
if (selectedRow?.mode === 'normal') void loadDetail(selectedRow);
|
|
725
1718
|
}),
|
|
726
1719
|
empty: emptyText,
|
|
727
1720
|
}));
|
|
1721
|
+
const pagination = paginationControls(doc, aggregate, () => {
|
|
1722
|
+
if (aggregate.pagination?.nextCursor) {
|
|
1723
|
+
state.cursor = aggregate.pagination.nextCursor;
|
|
1724
|
+
void load({ cursor: state.cursor, append: true });
|
|
1725
|
+
}
|
|
1726
|
+
});
|
|
1727
|
+
if (pagination) wrapper.append(pagination);
|
|
728
1728
|
const nextSearchInput = findFirst(wrapper, (node) => node.tagName === 'INPUT');
|
|
729
1729
|
if (previousSearchInput && nextSearchInput) {
|
|
730
1730
|
previousSearchInput.value = state.query;
|
|
@@ -740,7 +1740,23 @@ export function createJudgmentHistoryUI({
|
|
|
740
1740
|
state.focusSearch = false;
|
|
741
1741
|
}
|
|
742
1742
|
const selected = aggregate.rows.find((row) => row.key === state.selectedKey) ?? null;
|
|
743
|
-
|
|
1743
|
+
const selectedDetail = selected ? state.details.get(selected.recordId) ?? null : null;
|
|
1744
|
+
const railOptions = selected?.mode === 'normal'
|
|
1745
|
+
? {
|
|
1746
|
+
detailReady: state.detailPhase === 'ready' && Boolean(selectedDetail),
|
|
1747
|
+
detailPhase: state.detailPhase,
|
|
1748
|
+
draft: feedbackDraft(selected.recordId),
|
|
1749
|
+
onKindChange: (kind) => {
|
|
1750
|
+
updateFeedbackDraft(selected.recordId, { kind: feedbackKind(kind) ?? 'feedback' });
|
|
1751
|
+
render();
|
|
1752
|
+
},
|
|
1753
|
+
onSummaryChange: (summary) => updateFeedbackDraft(selected.recordId, { summary: String(summary ?? '') }),
|
|
1754
|
+
onOutcomeStatusChange: (outcomeStatus) => updateFeedbackDraft(selected.recordId, { outcomeStatus: String(outcomeStatus ?? '') }),
|
|
1755
|
+
onSubmitFeedback: (controls) => void submitFeedback(selected, controls),
|
|
1756
|
+
onCopyReference: (payload) => void copyReference(payload),
|
|
1757
|
+
}
|
|
1758
|
+
: {};
|
|
1759
|
+
renderRail(doc, rail, selected, selectedDetail, selected ? closeSelected : null, railOptions);
|
|
744
1760
|
if (state.lastFocusedKey) {
|
|
745
1761
|
const row = findFirst(root, (node) => attributeValue(node, 'data-key') === state.lastFocusedKey);
|
|
746
1762
|
row?.focus?.();
|
|
@@ -748,13 +1764,15 @@ export function createJudgmentHistoryUI({
|
|
|
748
1764
|
}
|
|
749
1765
|
}
|
|
750
1766
|
|
|
751
|
-
async function load() {
|
|
1767
|
+
async function load({ cursor = null, append = false } = {}) {
|
|
752
1768
|
if (state.disposed) return null;
|
|
753
1769
|
const generation = ++state.generation;
|
|
754
1770
|
state.phase = 'loading';
|
|
755
1771
|
state.error = null;
|
|
756
|
-
|
|
757
|
-
|
|
1772
|
+
if (!append) {
|
|
1773
|
+
state.home = null;
|
|
1774
|
+
state.aggregate = null;
|
|
1775
|
+
}
|
|
758
1776
|
render();
|
|
759
1777
|
if (!request) {
|
|
760
1778
|
if (generation !== state.generation || state.disposed) return null;
|
|
@@ -764,19 +1782,45 @@ export function createJudgmentHistoryUI({
|
|
|
764
1782
|
return null;
|
|
765
1783
|
}
|
|
766
1784
|
try {
|
|
767
|
-
|
|
1785
|
+
let activePath = normalizedBasePath;
|
|
1786
|
+
let response = await request(historyPath(normalizedBasePath, cursor), {
|
|
768
1787
|
method: 'GET',
|
|
769
1788
|
headers: token ? { 'x-brainbase-review-token': token } : undefined,
|
|
770
1789
|
});
|
|
771
1790
|
if (generation !== state.generation || state.disposed) return null;
|
|
1791
|
+
if (response?.status === 404 && legacyBasePath && !append && !state.project && !state.entrypoint && !state.cursor) {
|
|
1792
|
+
activePath = legacyBasePath;
|
|
1793
|
+
response = await request(`${legacyBasePath}/home`, {
|
|
1794
|
+
method: 'GET',
|
|
1795
|
+
headers: token ? { 'x-brainbase-review-token': token } : undefined,
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
772
1798
|
if (!response || response.ok !== true) throw new Error(`http_${response?.status ?? 0}`);
|
|
773
1799
|
const payload = await response.json();
|
|
774
1800
|
if (generation !== state.generation || state.disposed) return null;
|
|
775
|
-
|
|
1801
|
+
const normalized = normalizeJudgmentHistoryHome(payload);
|
|
1802
|
+
state.activeBasePath = activePath;
|
|
1803
|
+
if (append && state.home?.mode === 'normal' && normalized.mode === 'normal') {
|
|
1804
|
+
const records = [...state.home.records];
|
|
1805
|
+
const knownRecordIds = new Set(records.map((record) => record.recordId));
|
|
1806
|
+
for (const record of normalized.records) {
|
|
1807
|
+
if (knownRecordIds.has(record.recordId)) continue;
|
|
1808
|
+
knownRecordIds.add(record.recordId);
|
|
1809
|
+
records.push(record);
|
|
1810
|
+
}
|
|
1811
|
+
state.home = { ...normalized, records, coverage: normalized.coverage ?? state.home.coverage };
|
|
1812
|
+
} else {
|
|
1813
|
+
state.home = normalized;
|
|
1814
|
+
}
|
|
1815
|
+
if (state.home.mode === 'normal') {
|
|
1816
|
+
state.project = state.project ?? state.home.filters?.project ?? null;
|
|
1817
|
+
state.entrypoint = state.entrypoint ?? state.home.filters?.entrypoint ?? null;
|
|
1818
|
+
state.cursor = state.home.pagination?.nextCursor ?? null;
|
|
1819
|
+
}
|
|
776
1820
|
state.phase = 'ready';
|
|
777
1821
|
state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
|
|
778
1822
|
if (state.pendingDecisionId) {
|
|
779
|
-
const pending = state.aggregate.rows.find((row) => row.decisionAttemptId === state.pendingDecisionId);
|
|
1823
|
+
const pending = state.aggregate.rows.find((row) => row.decisionAttemptId === state.pendingDecisionId || row.recordId === state.pendingDecisionId);
|
|
780
1824
|
state.pendingDecisionId = null;
|
|
781
1825
|
if (pending) state.selectedKey = pending.key;
|
|
782
1826
|
}
|
|
@@ -798,11 +1842,12 @@ export function createJudgmentHistoryUI({
|
|
|
798
1842
|
state.pendingDecisionId = id;
|
|
799
1843
|
return true;
|
|
800
1844
|
}
|
|
801
|
-
const row = state.aggregate?.rows?.find((candidate) => candidate.decisionAttemptId === id);
|
|
1845
|
+
const row = state.aggregate?.rows?.find((candidate) => candidate.decisionAttemptId === id || candidate.recordId === id);
|
|
802
1846
|
if (!row) return false;
|
|
803
1847
|
state.selectedKey = row.key;
|
|
804
1848
|
state.lastFocusedKey = row.key;
|
|
805
1849
|
render();
|
|
1850
|
+
if (row.mode === 'normal') void loadDetail(row);
|
|
806
1851
|
return true;
|
|
807
1852
|
}
|
|
808
1853
|
|