@unson/brainbase-mcp 0.23.2 → 0.23.4
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-frame.d.ts +26 -1
- package/dist/judgment-frame.js +83 -1
- 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/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 +1095 -33
- 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);
|
|
@@ -459,10 +849,27 @@ function appendReferenceList(doc, references) {
|
|
|
459
849
|
return list;
|
|
460
850
|
}
|
|
461
851
|
|
|
852
|
+
function evidenceIdentifier(entry) {
|
|
853
|
+
const direct = text(entry);
|
|
854
|
+
if (direct) return direct;
|
|
855
|
+
if (!isRecord(entry)) return '';
|
|
856
|
+
const label = text(entry.label);
|
|
857
|
+
const kind = text(entry.kind);
|
|
858
|
+
const reference = text(entry.ref);
|
|
859
|
+
const identity = [kind, reference].filter(Boolean).join(':');
|
|
860
|
+
if (label && identity) return `${label}(${identity})`;
|
|
861
|
+
return label || identity;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function evidenceText(entry) {
|
|
865
|
+
const status = isRecord(entry) && entry.status === 'verified' ? '確認済み' : '未確認';
|
|
866
|
+
return `${evidenceIdentifier(entry) || '証拠の識別情報は未確認'}(${status})`;
|
|
867
|
+
}
|
|
868
|
+
|
|
462
869
|
function appendSupplement(doc, proof) {
|
|
463
870
|
const execution = isRecord(proof.execution) ? proof.execution : {};
|
|
464
871
|
const outcome = isRecord(proof.outcome) ? proof.outcome : {};
|
|
465
|
-
const evidence = Array.isArray(outcome.evidence_refs) ? outcome.evidence_refs.filter(Boolean).join('、') : '';
|
|
872
|
+
const evidence = Array.isArray(outcome.evidence_refs) ? outcome.evidence_refs.filter(Boolean).map(evidenceText).join('、') : '';
|
|
466
873
|
return workspaceDefinition(doc, [
|
|
467
874
|
['実行', [text(execution.status) || '未記録', text(execution.summary)]],
|
|
468
875
|
['結果', [text(outcome.status) || '未記録', text(outcome.summary), evidence ? `証拠: ${evidence}` : '']],
|
|
@@ -470,6 +877,49 @@ function appendSupplement(doc, proof) {
|
|
|
470
877
|
]);
|
|
471
878
|
}
|
|
472
879
|
|
|
880
|
+
function appendNormalAlternatives(doc, alternatives) {
|
|
881
|
+
if (alternatives === null || alternatives === undefined) {
|
|
882
|
+
return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '案は記録されていません。' });
|
|
883
|
+
}
|
|
884
|
+
if (!alternatives.length) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '保存された案はありません。' });
|
|
885
|
+
const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-alternative-list' });
|
|
886
|
+
for (const alternative of alternatives) {
|
|
887
|
+
const item = makeWorkspaceElement(doc, 'li');
|
|
888
|
+
item.append(makeWorkspaceElement(doc, 'strong', { text: alternative.label || '案の名称は未記録' }));
|
|
889
|
+
if (alternative.evaluation) item.append(makeWorkspaceElement(doc, 'span', { text: `評価: ${alternative.evaluation}` }));
|
|
890
|
+
if (alternative.adoption) item.append(makeWorkspaceElement(doc, 'span', { text: `採用状態: ${alternative.adoption}` }));
|
|
891
|
+
else if (alternative.adopted === true) item.append(makeWorkspaceElement(doc, 'span', { text: '採用状態: 採用' }));
|
|
892
|
+
else if (alternative.adopted === false) item.append(makeWorkspaceElement(doc, 'span', { text: '採用状態: 不採用' }));
|
|
893
|
+
else if (Object.prototype.hasOwnProperty.call(alternative, 'adopted')) item.append(makeWorkspaceElement(doc, 'span', { text: '採用状態: 記録なし' }));
|
|
894
|
+
list.append(item);
|
|
895
|
+
}
|
|
896
|
+
return list;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function normalExecutionLabel(execution) {
|
|
900
|
+
const status = text(execution?.status).toLowerCase();
|
|
901
|
+
const labels = { pending: '実行待ち', completed: '実行済み', failed: '実行失敗', held: '保留', cancelled: '取消', unknown: '実行状態未確認' };
|
|
902
|
+
return labels[status] ?? '実行状態未確認';
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function normalOutcomeLabel(execution) {
|
|
906
|
+
const status = text(execution?.outcome_status).toLowerCase();
|
|
907
|
+
const labels = { confirmed: '結果確認済み', unconfirmed: '結果未確認', unknown: '結果未確認' };
|
|
908
|
+
return labels[status] ?? '結果未確認';
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function appendNormalExecution(doc, execution) {
|
|
912
|
+
return workspaceDefinition(doc, [
|
|
913
|
+
['実行', [normalExecutionLabel(execution), text(execution?.result_summary)]],
|
|
914
|
+
['結果', normalOutcomeLabel(execution)],
|
|
915
|
+
]);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function appendNormalMissingFields(doc, missingFields) {
|
|
919
|
+
if (!Array.isArray(missingFields) || missingFields.length === 0) return null;
|
|
920
|
+
return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-missing-fields', text: `未記録: ${missingFields.join('、')}` });
|
|
921
|
+
}
|
|
922
|
+
|
|
473
923
|
function appendTextParts(doc, values) {
|
|
474
924
|
const wrap = makeWorkspaceElement(doc, 'div');
|
|
475
925
|
for (const value of values) {
|
|
@@ -478,7 +928,209 @@ function appendTextParts(doc, values) {
|
|
|
478
928
|
return wrap;
|
|
479
929
|
}
|
|
480
930
|
|
|
481
|
-
function
|
|
931
|
+
function normalFeedbackContentSummary(content) {
|
|
932
|
+
if (!isRecord(content)) return '内容は未記録';
|
|
933
|
+
return text(content.summary ?? content.result_summary ?? content.message ?? content.reason)
|
|
934
|
+
|| canonicalJson(content)
|
|
935
|
+
|| '内容は未記録';
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function normalReferencePayload(record, event, applicabilityReason = '', eventRef = null) {
|
|
939
|
+
const payload = {
|
|
940
|
+
ref: `judgment-history:${record.recordId}`,
|
|
941
|
+
record_id: record.recordId,
|
|
942
|
+
why: text(applicabilityReason) || null,
|
|
943
|
+
};
|
|
944
|
+
if (event) {
|
|
945
|
+
payload.event_ref = eventRef;
|
|
946
|
+
payload.event_id = event.eventId;
|
|
947
|
+
payload.kind = event.kind;
|
|
948
|
+
}
|
|
949
|
+
return payload;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function normalReferencePayloadText(record, event, applicabilityReason = '', eventRef = null) {
|
|
953
|
+
return JSON.stringify(normalReferencePayload(record, event, applicabilityReason, eventRef), null, 2);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function appendNormalReferenceExport(doc, record, event, { onCopyReference } = {}) {
|
|
957
|
+
const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-reference-export' });
|
|
958
|
+
const refLabel = event
|
|
959
|
+
? `${record.recordId} / ${event.eventId}`
|
|
960
|
+
: `${record.recordId} / イベントIDは未記録`;
|
|
961
|
+
wrap.append(makeWorkspaceElement(doc, 'code', { text: refLabel }));
|
|
962
|
+
if (event) wrap.append(makeWorkspaceElement(doc, 'p', { text: `種別: ${FEEDBACK_KIND_LABELS[event.kind] ?? event.kind}・${normalFeedbackContentSummary(event.content)}` }));
|
|
963
|
+
const reason = makeWorkspaceElement(doc, 'textarea', {
|
|
964
|
+
className: 'bb-jh-applicability-reason',
|
|
965
|
+
attrs: {
|
|
966
|
+
rows: 2,
|
|
967
|
+
'aria-label': `適用理由${event ? `(${event.eventId})` : ''}`,
|
|
968
|
+
placeholder: '次の判断で使う理由を記入',
|
|
969
|
+
},
|
|
970
|
+
});
|
|
971
|
+
reason.value = '';
|
|
972
|
+
wrap.append(makeWorkspaceElement(doc, 'label', { text: '適用理由' }), reason);
|
|
973
|
+
const preview = makeWorkspaceElement(doc, 'pre', {
|
|
974
|
+
className: 'bb-jh-reference-payload',
|
|
975
|
+
text: normalReferencePayloadText(record, event),
|
|
976
|
+
});
|
|
977
|
+
const status = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-copy-status', text: 'この情報を次の判断へ渡せます。' });
|
|
978
|
+
wrap.append(preview, workspaceButton(doc, {
|
|
979
|
+
text: '参照情報をコピー',
|
|
980
|
+
variant: 'quiet',
|
|
981
|
+
attrs: { 'data-action': 'copy-judgment-reference' },
|
|
982
|
+
onClick: () => {
|
|
983
|
+
if (typeof onCopyReference === 'function') onCopyReference({ record, event, reason, preview, status });
|
|
984
|
+
else status.textContent = '参照情報を選択してコピーできます。';
|
|
985
|
+
},
|
|
986
|
+
}), status);
|
|
987
|
+
return wrap;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function appendNormalFeedbackEvents(doc, record, { onCopyReference } = {}) {
|
|
991
|
+
const events = Array.isArray(record.feedbackEvents) ? record.feedbackEvents : [];
|
|
992
|
+
const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-feedback-events' });
|
|
993
|
+
if (!events.length) {
|
|
994
|
+
wrap.append(makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '追記された結果・訂正・フィードバックはありません。' }));
|
|
995
|
+
wrap.append(appendNormalReferenceExport(doc, record, null, { onCopyReference }));
|
|
996
|
+
return wrap;
|
|
997
|
+
}
|
|
998
|
+
for (const event of events) {
|
|
999
|
+
const item = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-feedback-event' });
|
|
1000
|
+
item.append(makeWorkspaceElement(doc, 'strong', { text: FEEDBACK_KIND_LABELS[event.kind] ?? event.kind }));
|
|
1001
|
+
item.append(makeWorkspaceElement(doc, 'code', { text: `record_id=${event.recordId} event_id=${event.eventId}` }));
|
|
1002
|
+
item.append(makeWorkspaceElement(doc, 'p', { text: normalFeedbackContentSummary(event.content) }));
|
|
1003
|
+
item.append(appendNormalReferenceExport(doc, record, event, { onCopyReference }));
|
|
1004
|
+
wrap.append(item);
|
|
1005
|
+
}
|
|
1006
|
+
return wrap;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function feedbackStatusText(draft) {
|
|
1010
|
+
if (!draft) return '';
|
|
1011
|
+
if (draft.phase === 'submitting') return '送信中です。保存状態はまだ未確認です。';
|
|
1012
|
+
if (draft.phase === 'readback') return '正本を読み戻して保存状態を確認しています。';
|
|
1013
|
+
if (draft.phase === 'saved') return '保存済み(正本で確認しました)。';
|
|
1014
|
+
if (draft.phase === 'unavailable') return `利用できません。${draft.status || '正本に接続できないため、保存状態を確認できません。'}`;
|
|
1015
|
+
if (draft.phase === 'unconfirmed') return `未確認。${draft.status || 'POST後の正本読戻しで一致を確認できませんでした。'}`;
|
|
1016
|
+
if (draft.phase === 'error') return `送信できません。保存状態は未確認です。${draft.status ? ` ${draft.status}` : ''}`;
|
|
1017
|
+
return draft.status || '送信前です。';
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function appendNormalFeedbackComposer(doc, record, {
|
|
1021
|
+
detailReady = false,
|
|
1022
|
+
detailPhase = 'idle',
|
|
1023
|
+
draft,
|
|
1024
|
+
onKindChange,
|
|
1025
|
+
onSummaryChange,
|
|
1026
|
+
onOutcomeStatusChange,
|
|
1027
|
+
onSubmitFeedback,
|
|
1028
|
+
} = {}) {
|
|
1029
|
+
const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-feedback-composer' });
|
|
1030
|
+
if (!detailReady) {
|
|
1031
|
+
const message = detailPhase === 'loading'
|
|
1032
|
+
? '正本の詳細を確認しています。'
|
|
1033
|
+
: '正本の詳細を確認できないため、追記は利用できません。';
|
|
1034
|
+
wrap.append(makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: message }));
|
|
1035
|
+
return wrap;
|
|
1036
|
+
}
|
|
1037
|
+
const kindLabel = makeWorkspaceElement(doc, 'label', { text: '追記の種別' });
|
|
1038
|
+
const kind = makeWorkspaceElement(doc, 'select', { attrs: { 'aria-label': '追記の種別' } });
|
|
1039
|
+
for (const value of FEEDBACK_KINDS) {
|
|
1040
|
+
kind.append(makeWorkspaceElement(doc, 'option', {
|
|
1041
|
+
text: FEEDBACK_KIND_LABELS[value],
|
|
1042
|
+
attrs: { value },
|
|
1043
|
+
}));
|
|
1044
|
+
}
|
|
1045
|
+
kind.value = draft?.kind ?? 'feedback';
|
|
1046
|
+
kind.addEventListener('change', (event) => onKindChange?.(event?.target?.value ?? kind.value));
|
|
1047
|
+
const summaryLabel = makeWorkspaceElement(doc, 'label', { text: '内容(保存公開フィールド)' });
|
|
1048
|
+
const summary = makeWorkspaceElement(doc, 'textarea', {
|
|
1049
|
+
className: 'bb-jh-feedback-content',
|
|
1050
|
+
attrs: { rows: 3, 'aria-label': '追記の内容', placeholder: '結果・訂正・フィードバックを記入' },
|
|
1051
|
+
});
|
|
1052
|
+
summary.value = draft?.summary ?? '';
|
|
1053
|
+
summary.addEventListener('input', (event) => onSummaryChange?.(event?.target?.value ?? summary.value));
|
|
1054
|
+
const outcomeLabel = makeWorkspaceElement(doc, 'label', { text: '結果の確認状態(明示する場合)' });
|
|
1055
|
+
const outcomeStatus = makeWorkspaceElement(doc, 'select', { attrs: { 'aria-label': '結果の確認状態' } });
|
|
1056
|
+
outcomeStatus.append(makeWorkspaceElement(doc, 'option', { text: '記録しない', attrs: { value: '' } }));
|
|
1057
|
+
for (const value of ['unknown', 'unconfirmed', 'confirmed']) {
|
|
1058
|
+
const label = { unknown: '未確認', unconfirmed: '結果未確認', confirmed: '結果確認済み' }[value];
|
|
1059
|
+
outcomeStatus.append(makeWorkspaceElement(doc, 'option', { text: label, attrs: { value } }));
|
|
1060
|
+
}
|
|
1061
|
+
outcomeStatus.value = draft?.outcomeStatus ?? '';
|
|
1062
|
+
outcomeStatus.addEventListener('change', (event) => onOutcomeStatusChange?.(event?.target?.value ?? outcomeStatus.value));
|
|
1063
|
+
const eventId = makeWorkspaceElement(doc, 'code', { text: `event_id=${draft?.eventId ?? '未発行'}` });
|
|
1064
|
+
const phase = draft?.phase ?? 'idle';
|
|
1065
|
+
const busy = phase === 'submitting' || phase === 'readback';
|
|
1066
|
+
const button = workspaceButton(doc, {
|
|
1067
|
+
text: phase === 'saved' ? '保存済み' : '結果・訂正・フィードバックを追記',
|
|
1068
|
+
variant: 'primary',
|
|
1069
|
+
disabled: busy || phase === 'saved',
|
|
1070
|
+
attrs: { 'data-action': 'submit-judgment-feedback' },
|
|
1071
|
+
onClick: () => onSubmitFeedback?.({ kind, summary, outcomeStatus, eventId: draft?.eventId }),
|
|
1072
|
+
});
|
|
1073
|
+
const status = makeWorkspaceElement(doc, 'p', {
|
|
1074
|
+
className: `bb-jh-feedback-status is-${phase}`,
|
|
1075
|
+
text: feedbackStatusText(draft),
|
|
1076
|
+
attrs: { role: phase === 'error' || phase === 'unavailable' ? 'alert' : 'status' },
|
|
1077
|
+
});
|
|
1078
|
+
wrap.append(kindLabel, kind, summaryLabel, summary, outcomeLabel, outcomeStatus, eventId, button, status);
|
|
1079
|
+
return wrap;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function renderNormalRail(doc, rail, row, detail, onClose, options = {}) {
|
|
1083
|
+
if (!rail) return;
|
|
1084
|
+
rail.replaceChildren();
|
|
1085
|
+
const record = detail ?? row;
|
|
1086
|
+
rail.append(workspaceRailHead(doc, {
|
|
1087
|
+
kicker: `${NORMAL_ENTRYPOINT_LABELS[record.entrypoint] ?? '入口不明'}の判断`,
|
|
1088
|
+
title: text(record.judgment?.summary) || '判断内容は未記録',
|
|
1089
|
+
sub: formatDate(record.recordedAt),
|
|
1090
|
+
}));
|
|
1091
|
+
if (typeof onClose === 'function') {
|
|
1092
|
+
rail.append(workspaceButton(doc, {
|
|
1093
|
+
text: '詳細を閉じる',
|
|
1094
|
+
variant: 'quiet',
|
|
1095
|
+
onClick: onClose,
|
|
1096
|
+
attrs: { 'aria-label': '判断の詳細を閉じる' },
|
|
1097
|
+
}));
|
|
1098
|
+
}
|
|
1099
|
+
rail.append(workspaceRailBlock(doc, {
|
|
1100
|
+
title: '使った参照(当時の記録)',
|
|
1101
|
+
content: appendReferenceList(doc, record.selectedReferences),
|
|
1102
|
+
}));
|
|
1103
|
+
rail.append(workspaceRailBlock(doc, {
|
|
1104
|
+
title: '判断の理由',
|
|
1105
|
+
content: appendTextParts(doc, [text(record.judgment?.reason) || '理由は記録されていません。']),
|
|
1106
|
+
}));
|
|
1107
|
+
rail.append(workspaceRailBlock(doc, {
|
|
1108
|
+
title: '保存された案',
|
|
1109
|
+
content: appendNormalAlternatives(doc, record.alternatives),
|
|
1110
|
+
}));
|
|
1111
|
+
rail.append(workspaceRailBlock(doc, {
|
|
1112
|
+
title: '実行と結果',
|
|
1113
|
+
content: appendNormalExecution(doc, record.execution),
|
|
1114
|
+
}));
|
|
1115
|
+
rail.append(workspaceRailBlock(doc, {
|
|
1116
|
+
title: '結果・訂正・フィードバック',
|
|
1117
|
+
content: appendNormalFeedbackEvents(doc, record, { onCopyReference: options.onCopyReference }),
|
|
1118
|
+
}));
|
|
1119
|
+
rail.append(workspaceRailBlock(doc, {
|
|
1120
|
+
title: '次の判断に渡す参照',
|
|
1121
|
+
content: appendNormalFeedbackComposer(doc, record, options),
|
|
1122
|
+
}));
|
|
1123
|
+
const metadata = workspaceDefinition(doc, [
|
|
1124
|
+
['記録ID', record.recordId],
|
|
1125
|
+
['プロジェクト', record.projectCode],
|
|
1126
|
+
['Turn', record.turnRef],
|
|
1127
|
+
]);
|
|
1128
|
+
const missing = appendNormalMissingFields(doc, record.missingFields);
|
|
1129
|
+
if (missing) metadata.append(missing);
|
|
1130
|
+
rail.append(workspaceRailBlock(doc, { title: '記録の範囲', content: metadata }));
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function renderRail(doc, rail, row, detail, onClose, options = {}) {
|
|
482
1134
|
if (!rail) return;
|
|
483
1135
|
rail.replaceChildren();
|
|
484
1136
|
if (!row) {
|
|
@@ -489,6 +1141,10 @@ function renderRail(doc, rail, row, onClose) {
|
|
|
489
1141
|
}));
|
|
490
1142
|
return;
|
|
491
1143
|
}
|
|
1144
|
+
if (row.mode === 'normal') {
|
|
1145
|
+
renderNormalRail(doc, rail, row, detail, onClose, options);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
492
1148
|
const proof = row.proof;
|
|
493
1149
|
rail.append(workspaceRailHead(doc, {
|
|
494
1150
|
kicker: row.delegated ? 'Brainbaseの判断' : '判断履歴',
|
|
@@ -531,9 +1187,16 @@ function statusNotice(doc, phase, error, home, onRetry) {
|
|
|
531
1187
|
tone: 'danger',
|
|
532
1188
|
role: 'alert',
|
|
533
1189
|
});
|
|
1190
|
+
if (home?.status === 'error') return workspaceNotice(doc, {
|
|
1191
|
+
label: '取得に失敗',
|
|
1192
|
+
text: ['判断の記録を確認できません。データが0件とは確認できていません。', workspaceButton(doc, { text: '再読み込み', variant: 'quiet', onClick: onRetry })],
|
|
1193
|
+
tone: 'danger',
|
|
1194
|
+
role: 'alert',
|
|
1195
|
+
});
|
|
1196
|
+
const sourceSummary = normalSourcesSummary(home);
|
|
534
1197
|
if (home?.status === 'unavailable') return workspaceNotice(doc, {
|
|
535
1198
|
label: '未接続',
|
|
536
|
-
text: ['
|
|
1199
|
+
text: [`保存された判断に接続できません${home.coverage?.storage ? `(${normalStorageLabel(home.coverage.storage)})` : ''}。データが0件とは確認できていません。`, sourceSummary ? `取得元: ${sourceSummary}` : null, workspaceButton(doc, { text: '再読み込み', variant: 'quiet', onClick: onRetry })],
|
|
537
1200
|
tone: 'warning',
|
|
538
1201
|
role: 'alert',
|
|
539
1202
|
});
|
|
@@ -545,7 +1208,7 @@ function statusNotice(doc, phase, error, home, onRetry) {
|
|
|
545
1208
|
});
|
|
546
1209
|
if (home?.status === 'partial') return workspaceNotice(doc, {
|
|
547
1210
|
label: '取得できた範囲',
|
|
548
|
-
text: [`読み取れた判断だけを表示しています。${home.coverage?.rejected ?
|
|
1211
|
+
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
1212
|
tone: 'warning',
|
|
550
1213
|
role: 'status',
|
|
551
1214
|
});
|
|
@@ -577,12 +1240,108 @@ function createSearch(doc, value, onInput) {
|
|
|
577
1240
|
return label;
|
|
578
1241
|
}
|
|
579
1242
|
|
|
1243
|
+
function normalStorageLabel(storage) {
|
|
1244
|
+
if (storage === 'local') return '端末';
|
|
1245
|
+
if (storage === 'server') return 'サーバー';
|
|
1246
|
+
return storage || '接続先不明';
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function normalReasonLabel(reason) {
|
|
1250
|
+
const labels = {
|
|
1251
|
+
unconnected: '未接続',
|
|
1252
|
+
mana_not_connected: '未接続(Mana)',
|
|
1253
|
+
company_os_native_source_not_connected: 'Company OSの判断実行履歴が未接続',
|
|
1254
|
+
journal_unavailable: '判断記録がありません',
|
|
1255
|
+
journal_unreadable: '判断記録を読み取れません',
|
|
1256
|
+
};
|
|
1257
|
+
return labels[reason] ?? reason;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function normalSourceStatusLabel(status) {
|
|
1261
|
+
const labels = {
|
|
1262
|
+
available: '接続済み',
|
|
1263
|
+
partial: '一部取得',
|
|
1264
|
+
unavailable: '未接続',
|
|
1265
|
+
};
|
|
1266
|
+
return labels[status] ?? '未確認';
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function normalSourcesSummary(home) {
|
|
1270
|
+
if (home?.mode !== 'normal' || !Array.isArray(home.coverage?.sources) || home.coverage.sources.length === 0) return '';
|
|
1271
|
+
return home.coverage.sources.map((source) => {
|
|
1272
|
+
const entrypoint = NORMAL_ENTRYPOINT_LABELS[normalEntrypoint(source.entrypoint)] ?? '入口不明';
|
|
1273
|
+
const reason = source.reason ? `(${normalReasonLabel(source.reason)})` : '';
|
|
1274
|
+
return `${entrypoint}: ${normalSourceStatusLabel(source.status)}${reason}`;
|
|
1275
|
+
}).join('、');
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function createFilterSelect(doc, { label, value, options, onChange, className = '' }) {
|
|
1279
|
+
const wrap = makeWorkspaceElement(doc, 'label', { className: `bb-jh-filter${className ? ` ${className}` : ''}` });
|
|
1280
|
+
wrap.append(makeWorkspaceElement(doc, 'span', { text: label }));
|
|
1281
|
+
const select = makeWorkspaceElement(doc, 'select', { attrs: { 'aria-label': label } });
|
|
1282
|
+
const current = value ?? '';
|
|
1283
|
+
const choices = [{ value: '', label: 'すべて' }, ...options];
|
|
1284
|
+
for (const option of choices) {
|
|
1285
|
+
const optionElement = makeWorkspaceElement(doc, 'option', { text: option.label, attrs: { value: option.value, selected: option.value === current ? 'selected' : undefined } });
|
|
1286
|
+
optionElement.value = option.value;
|
|
1287
|
+
select.append(optionElement);
|
|
1288
|
+
}
|
|
1289
|
+
select.value = current;
|
|
1290
|
+
select.addEventListener('change', (event) => onChange(event?.target?.value ?? select.value ?? ''));
|
|
1291
|
+
wrap.append(select);
|
|
1292
|
+
return wrap;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function normalFilterControls(doc, home, state, onChange) {
|
|
1296
|
+
const filters = home?.filters ?? {};
|
|
1297
|
+
const projects = [...(filters.projects ?? [])];
|
|
1298
|
+
const knownProjects = (home.records ?? []).map((record) => record.projectCode).filter(Boolean);
|
|
1299
|
+
for (const project of knownProjects) {
|
|
1300
|
+
if (!projects.some((option) => option.value === project)) projects.push({ value: project, label: project });
|
|
1301
|
+
}
|
|
1302
|
+
const entrypoints = [...(filters.entrypoints ?? [])];
|
|
1303
|
+
const knownEntrypoints = (home.records ?? []).map((record) => record.entrypoint).filter(Boolean);
|
|
1304
|
+
for (const entrypoint of [...NORMAL_ENTRYPOINTS, ...knownEntrypoints]) {
|
|
1305
|
+
if (!entrypoints.some((option) => option.value === entrypoint)) {
|
|
1306
|
+
entrypoints.push({ value: entrypoint, label: NORMAL_ENTRYPOINT_LABELS[entrypoint] ?? entrypoint });
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-normal-filters', attrs: { 'aria-label': '絞り込み' } });
|
|
1310
|
+
wrap.append(createFilterSelect(doc, {
|
|
1311
|
+
label: 'プロジェクト',
|
|
1312
|
+
value: state.project,
|
|
1313
|
+
options: projects,
|
|
1314
|
+
onChange: (project) => onChange({ project: project || null }),
|
|
1315
|
+
}));
|
|
1316
|
+
wrap.append(createFilterSelect(doc, {
|
|
1317
|
+
label: '入口',
|
|
1318
|
+
value: state.entrypoint,
|
|
1319
|
+
options: entrypoints,
|
|
1320
|
+
onChange: (entrypoint) => onChange({ entrypoint: entrypoint || null }),
|
|
1321
|
+
}));
|
|
1322
|
+
return wrap;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function paginationControls(doc, aggregate, onNext) {
|
|
1326
|
+
if (aggregate?.mode !== 'normal') return null;
|
|
1327
|
+
const pagination = aggregate?.pagination;
|
|
1328
|
+
const total = aggregate?.totalCount;
|
|
1329
|
+
if (!pagination?.hasNext && total === null) return null;
|
|
1330
|
+
const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-pagination' });
|
|
1331
|
+
const count = total === null ? '同じ範囲の件数は未確認' : `同じ範囲の件数: ${total}件`;
|
|
1332
|
+
wrap.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-pagination-count', text: count }));
|
|
1333
|
+
if (pagination?.hasNext) {
|
|
1334
|
+
wrap.append(workspaceButton(doc, { text: '次のページ', variant: 'quiet', onClick: onNext, attrs: { 'data-action': 'next-page' } }));
|
|
1335
|
+
}
|
|
1336
|
+
return wrap;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
580
1339
|
function makeRows(doc, aggregate, selectedKey, onSelect) {
|
|
581
1340
|
return aggregate.rows.map((row) => ({
|
|
582
1341
|
key: row.key,
|
|
583
|
-
label: `${formatDate(row.recordedAt)} ${text(row.proof?.decision?.summary) || '判断履歴'}`,
|
|
1342
|
+
label: `${formatDate(row.recordedAt)} ${row.mode === 'normal' ? text(row.judgment?.summary) || '判断履歴' : text(row.proof?.decision?.summary) || '判断履歴'}`,
|
|
584
1343
|
selected: row.key === selectedKey,
|
|
585
|
-
className: `bb-jh-row${row.delegated ? ' is-delegated' : ' is-returned'}`,
|
|
1344
|
+
className: `bb-jh-row${row.mode === 'normal' ? ' is-normal' : row.delegated ? ' is-delegated' : ' is-returned'}`,
|
|
586
1345
|
onSelect,
|
|
587
1346
|
cells: [
|
|
588
1347
|
makeWorkspaceElement(doc, 'span', { className: 'bb-jh-date', text: formatDate(row.recordedAt) }),
|
|
@@ -592,14 +1351,14 @@ function makeRows(doc, aggregate, selectedKey, onSelect) {
|
|
|
592
1351
|
}));
|
|
593
1352
|
}
|
|
594
1353
|
|
|
595
|
-
/** Create the shared
|
|
1354
|
+
/** Create the shared judgment history controller and canonical feedback adapter. */
|
|
596
1355
|
export function createJudgmentHistoryUI({
|
|
597
1356
|
root,
|
|
598
1357
|
rail = null,
|
|
599
1358
|
document: explicitDocument,
|
|
600
1359
|
fetcher,
|
|
601
1360
|
token,
|
|
602
|
-
basePath = '/api/
|
|
1361
|
+
basePath = '/api/judgment-history',
|
|
603
1362
|
page,
|
|
604
1363
|
autoLoad = true,
|
|
605
1364
|
pageHeader = true,
|
|
@@ -611,6 +1370,7 @@ export function createJudgmentHistoryUI({
|
|
|
611
1370
|
? fetcher
|
|
612
1371
|
: typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
|
|
613
1372
|
const normalizedBasePath = String(basePath).replace(/\/+$/u, '');
|
|
1373
|
+
const legacyBasePath = normalizedBasePath === '/api/judgment-history' ? '/api/value-proofs' : null;
|
|
614
1374
|
const readNow = () => (typeof now === 'function' ? now() : now);
|
|
615
1375
|
const state = {
|
|
616
1376
|
phase: autoLoad ? 'loading' : 'idle',
|
|
@@ -619,9 +1379,19 @@ export function createJudgmentHistoryUI({
|
|
|
619
1379
|
aggregate: null,
|
|
620
1380
|
period: PERIODS.past30days,
|
|
621
1381
|
query: '',
|
|
1382
|
+
project: null,
|
|
1383
|
+
entrypoint: null,
|
|
1384
|
+
cursor: null,
|
|
1385
|
+
limit: 50,
|
|
622
1386
|
selectedKey: null,
|
|
1387
|
+
details: new Map(),
|
|
1388
|
+
detailPhase: 'idle',
|
|
1389
|
+
detailError: null,
|
|
1390
|
+
activeBasePath: normalizedBasePath,
|
|
1391
|
+
feedbackDrafts: new Map(),
|
|
623
1392
|
disposed: false,
|
|
624
1393
|
generation: 0,
|
|
1394
|
+
detailGeneration: 0,
|
|
625
1395
|
focusSearch: false,
|
|
626
1396
|
lastFocusedKey: null,
|
|
627
1397
|
pendingDecisionId: null,
|
|
@@ -631,6 +1401,8 @@ export function createJudgmentHistoryUI({
|
|
|
631
1401
|
if (!state.selectedKey || state.disposed) return false;
|
|
632
1402
|
state.lastFocusedKey = state.selectedKey;
|
|
633
1403
|
state.selectedKey = null;
|
|
1404
|
+
state.detailPhase = 'idle';
|
|
1405
|
+
state.detailError = null;
|
|
634
1406
|
render();
|
|
635
1407
|
return true;
|
|
636
1408
|
}
|
|
@@ -650,6 +1422,229 @@ export function createJudgmentHistoryUI({
|
|
|
650
1422
|
};
|
|
651
1423
|
}
|
|
652
1424
|
|
|
1425
|
+
function newFeedbackEventId() {
|
|
1426
|
+
try {
|
|
1427
|
+
if (typeof globalThis.crypto?.randomUUID === 'function') return globalThis.crypto.randomUUID();
|
|
1428
|
+
} catch {
|
|
1429
|
+
// The fallback below is sufficient for a retry token when Web Crypto is unavailable.
|
|
1430
|
+
}
|
|
1431
|
+
return `judgment-history-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function feedbackDraft(recordId) {
|
|
1435
|
+
if (!recordId) return null;
|
|
1436
|
+
let draft = state.feedbackDrafts.get(recordId);
|
|
1437
|
+
if (!draft) {
|
|
1438
|
+
draft = {
|
|
1439
|
+
recordId,
|
|
1440
|
+
eventId: newFeedbackEventId(),
|
|
1441
|
+
kind: 'feedback',
|
|
1442
|
+
summary: '',
|
|
1443
|
+
outcomeStatus: '',
|
|
1444
|
+
phase: 'idle',
|
|
1445
|
+
status: '',
|
|
1446
|
+
};
|
|
1447
|
+
state.feedbackDrafts.set(recordId, draft);
|
|
1448
|
+
}
|
|
1449
|
+
return draft;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function updateFeedbackDraft(recordId, changes) {
|
|
1453
|
+
const current = feedbackDraft(recordId);
|
|
1454
|
+
if (!current || !isRecord(changes)) return current;
|
|
1455
|
+
const next = { ...current, ...changes };
|
|
1456
|
+
state.feedbackDrafts.set(recordId, next);
|
|
1457
|
+
return next;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function feedbackReadbackMatches(detail, expected) {
|
|
1461
|
+
if (!detail || !expected) return false;
|
|
1462
|
+
const events = Array.isArray(detail.feedbackEvents) ? detail.feedbackEvents : [];
|
|
1463
|
+
return events.some((event) => event.recordId === expected.recordId
|
|
1464
|
+
&& event.eventId === expected.eventId
|
|
1465
|
+
&& event.kind === expected.kind
|
|
1466
|
+
&& canonicalJson(event.content) === canonicalJson(expected.content));
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function responseFeedbackReadback(payload, expected) {
|
|
1470
|
+
if (!isRecord(payload) || !expected) return false;
|
|
1471
|
+
const candidate = payload.event
|
|
1472
|
+
?? payload.feedback_event
|
|
1473
|
+
?? (isRecord(payload.feedback) ? payload.feedback : null)
|
|
1474
|
+
?? (text(payload.event_id) ? payload : null);
|
|
1475
|
+
const event = normalizeFeedbackEvent(candidate, expected.recordId);
|
|
1476
|
+
if (!event) return false;
|
|
1477
|
+
const storageStatus = text(payload.storage_status ?? payload.storage?.status ?? payload.save_status).toLowerCase();
|
|
1478
|
+
const saved = payload.saved === true || event.saved === true || ['saved', 'stored', 'idempotent', 'confirmed'].includes(storageStatus)
|
|
1479
|
+
|| ['saved', 'stored', 'idempotent', 'confirmed'].includes(event.storageStatus)
|
|
1480
|
+
|| text(payload.status).toLowerCase() === 'saved';
|
|
1481
|
+
return saved
|
|
1482
|
+
&& event.recordId === expected.recordId
|
|
1483
|
+
&& event.eventId === expected.eventId
|
|
1484
|
+
&& event.kind === expected.kind
|
|
1485
|
+
&& canonicalJson(event.content) === canonicalJson(expected.content);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
async function feedbackReference(record, event) {
|
|
1489
|
+
if (!record || !event) return null;
|
|
1490
|
+
try {
|
|
1491
|
+
const subtle = globalThis.crypto?.subtle;
|
|
1492
|
+
if (!subtle || typeof subtle.digest !== 'function' || typeof TextEncoder !== 'function') return null;
|
|
1493
|
+
const bytes = await subtle.digest('SHA-256', new TextEncoder().encode(event.eventId));
|
|
1494
|
+
const hash = [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
1495
|
+
return `judgment-history-feedback:${record.recordId}:${hash}`;
|
|
1496
|
+
} catch {
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
async function copyReference({ record, event, reason, preview, status } = {}) {
|
|
1502
|
+
if (!record || !preview || !status) return false;
|
|
1503
|
+
const applicabilityReason = reason?.value ?? '';
|
|
1504
|
+
preview.textContent = normalReferencePayloadText(record, event, applicabilityReason);
|
|
1505
|
+
const eventRef = await feedbackReference(record, event);
|
|
1506
|
+
const serialized = normalReferencePayloadText(record, event, applicabilityReason, eventRef);
|
|
1507
|
+
preview.textContent = serialized;
|
|
1508
|
+
try {
|
|
1509
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
1510
|
+
if (!clipboard || typeof clipboard.writeText !== 'function') throw new Error('clipboard_unavailable');
|
|
1511
|
+
await clipboard.writeText(serialized);
|
|
1512
|
+
status.textContent = '参照情報をコピーしました。';
|
|
1513
|
+
return true;
|
|
1514
|
+
} catch {
|
|
1515
|
+
status.textContent = '参照情報を表示しました。選択してコピーできます。';
|
|
1516
|
+
return false;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
async function submitFeedback(row, controls) {
|
|
1521
|
+
if (!row || row.mode !== 'normal' || !controls || !request || state.disposed) return false;
|
|
1522
|
+
const current = feedbackDraft(row.recordId);
|
|
1523
|
+
const kind = feedbackKind(controls.kind?.value ?? current?.kind) ?? 'feedback';
|
|
1524
|
+
const summary = text(controls.summary?.value ?? current?.summary);
|
|
1525
|
+
const outcomeStatus = text(controls.outcomeStatus?.value ?? current?.outcomeStatus).toLowerCase();
|
|
1526
|
+
if (!summary) {
|
|
1527
|
+
updateFeedbackDraft(row.recordId, { kind, summary, outcomeStatus: '', phase: 'error', status: '内容を入力してください。' });
|
|
1528
|
+
render();
|
|
1529
|
+
return false;
|
|
1530
|
+
}
|
|
1531
|
+
const acceptedOutcomeStatuses = new Set(['unknown', 'unconfirmed', 'confirmed']);
|
|
1532
|
+
const content = { summary };
|
|
1533
|
+
if (acceptedOutcomeStatuses.has(outcomeStatus)) content.outcome_status = outcomeStatus;
|
|
1534
|
+
const displayedEventId = text(controls.eventId?.textContent);
|
|
1535
|
+
const eventId = displayedEventId.startsWith('event_id=')
|
|
1536
|
+
? displayedEventId.slice('event_id='.length)
|
|
1537
|
+
: displayedEventId || current?.eventId || newFeedbackEventId();
|
|
1538
|
+
const expected = { recordId: row.recordId, eventId, kind, content };
|
|
1539
|
+
updateFeedbackDraft(row.recordId, { eventId, kind, summary, outcomeStatus, phase: 'submitting', status: '' });
|
|
1540
|
+
render();
|
|
1541
|
+
try {
|
|
1542
|
+
const response = await request(`${state.activeBasePath}/feedback`, {
|
|
1543
|
+
method: 'POST',
|
|
1544
|
+
headers: {
|
|
1545
|
+
'content-type': 'application/json',
|
|
1546
|
+
...(token ? { 'x-brainbase-review-token': token } : {}),
|
|
1547
|
+
},
|
|
1548
|
+
body: JSON.stringify({ record_id: row.recordId, event_id: eventId, kind, content }),
|
|
1549
|
+
});
|
|
1550
|
+
if (!response || response.ok !== true) {
|
|
1551
|
+
const statusCode = response?.status ?? 0;
|
|
1552
|
+
const unavailable = [401, 403, 404, 405, 501, 503].includes(statusCode);
|
|
1553
|
+
updateFeedbackDraft(row.recordId, {
|
|
1554
|
+
phase: unavailable ? 'unavailable' : 'unconfirmed',
|
|
1555
|
+
status: unavailable ? `正本へ接続できません(HTTP ${statusCode})。` : `受付応答はHTTP ${statusCode}でした。`,
|
|
1556
|
+
});
|
|
1557
|
+
render();
|
|
1558
|
+
return false;
|
|
1559
|
+
}
|
|
1560
|
+
let responsePayload = null;
|
|
1561
|
+
if (typeof response.json === 'function') {
|
|
1562
|
+
try { responsePayload = await response.json(); } catch { responsePayload = null; }
|
|
1563
|
+
}
|
|
1564
|
+
updateFeedbackDraft(row.recordId, { phase: 'readback', status: '' });
|
|
1565
|
+
render();
|
|
1566
|
+
const detail = await loadDetail(row);
|
|
1567
|
+
const detailReadback = feedbackReadbackMatches(detail, expected);
|
|
1568
|
+
const responseReadback = responseFeedbackReadback(responsePayload, expected);
|
|
1569
|
+
if (detailReadback || responseReadback) {
|
|
1570
|
+
updateFeedbackDraft(row.recordId, { phase: 'saved', status: '' });
|
|
1571
|
+
} else if (state.detailPhase === 'error') {
|
|
1572
|
+
updateFeedbackDraft(row.recordId, { phase: 'unavailable', status: '正本を読み戻せませんでした。' });
|
|
1573
|
+
} else {
|
|
1574
|
+
updateFeedbackDraft(row.recordId, { phase: 'unconfirmed', status: '同じevent_idの正本記録を確認できませんでした。' });
|
|
1575
|
+
}
|
|
1576
|
+
render();
|
|
1577
|
+
return detailReadback || responseReadback;
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
updateFeedbackDraft(row.recordId, {
|
|
1580
|
+
phase: 'unavailable',
|
|
1581
|
+
status: `正本へ接続できません。${error instanceof Error ? error.message : String(error)}`,
|
|
1582
|
+
});
|
|
1583
|
+
render();
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function historyPath(path, cursor = null) {
|
|
1589
|
+
const params = new URLSearchParams();
|
|
1590
|
+
params.set('period', state.period);
|
|
1591
|
+
if (state.project) params.set('project', state.project);
|
|
1592
|
+
if (state.entrypoint) params.set('entrypoint', state.entrypoint);
|
|
1593
|
+
params.set('limit', String(state.limit));
|
|
1594
|
+
if (cursor) params.set('cursor', cursor);
|
|
1595
|
+
return `${path}/home?${params.toString()}`;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function normalMode() {
|
|
1599
|
+
return state.home?.mode === 'normal' || state.aggregate?.mode === 'normal';
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function handleNormalFilterChange(changes) {
|
|
1603
|
+
if (!normalMode()) return;
|
|
1604
|
+
if (Object.prototype.hasOwnProperty.call(changes, 'project')) state.project = changes.project;
|
|
1605
|
+
if (Object.prototype.hasOwnProperty.call(changes, 'entrypoint')) state.entrypoint = changes.entrypoint;
|
|
1606
|
+
state.cursor = null;
|
|
1607
|
+
state.selectedKey = null;
|
|
1608
|
+
state.details = new Map();
|
|
1609
|
+
void load({ append: false });
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
async function loadDetail(row) {
|
|
1613
|
+
if (!row || row.mode !== 'normal' || !request || state.disposed) return null;
|
|
1614
|
+
const generation = ++state.detailGeneration;
|
|
1615
|
+
state.detailPhase = 'loading';
|
|
1616
|
+
state.detailError = null;
|
|
1617
|
+
render();
|
|
1618
|
+
try {
|
|
1619
|
+
const response = await request(`${state.activeBasePath}/records/${encodeURIComponent(row.recordId)}`, {
|
|
1620
|
+
method: 'GET',
|
|
1621
|
+
headers: token ? { 'x-brainbase-review-token': token } : undefined,
|
|
1622
|
+
});
|
|
1623
|
+
if (generation !== state.detailGeneration || state.disposed) return null;
|
|
1624
|
+
if (!response || response.ok !== true) throw new Error(`http_${response?.status ?? 0}`);
|
|
1625
|
+
const payload = await response.json();
|
|
1626
|
+
if (generation !== state.detailGeneration || state.disposed) return null;
|
|
1627
|
+
let rawRecord = isRecord(payload?.record) ? { ...payload.record } : isRecord(payload) && text(payload.record_id) ? payload : null;
|
|
1628
|
+
if (rawRecord && isRecord(payload)) {
|
|
1629
|
+
for (const key of ['feedback_events', 'feedback', 'events', 'appended_events', 'event', 'feedback_event']) {
|
|
1630
|
+
if (rawRecord[key] === undefined && payload[key] !== undefined) rawRecord[key] = payload[key];
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
const detail = normalizeNormalRecord(rawRecord);
|
|
1634
|
+
if (!detail) throw new Error('record_response_invalid');
|
|
1635
|
+
state.details.set(row.recordId, detail);
|
|
1636
|
+
state.detailPhase = 'ready';
|
|
1637
|
+
render();
|
|
1638
|
+
return detail;
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
if (generation !== state.detailGeneration || state.disposed) return null;
|
|
1641
|
+
state.detailPhase = 'error';
|
|
1642
|
+
state.detailError = error instanceof Error ? error.message : String(error);
|
|
1643
|
+
render();
|
|
1644
|
+
return null;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
|
|
653
1648
|
function render() {
|
|
654
1649
|
if (state.disposed) return;
|
|
655
1650
|
const previousSearchInput = findFirst(root, (node) => node.tagName === 'INPUT');
|
|
@@ -672,19 +1667,24 @@ export function createJudgmentHistoryUI({
|
|
|
672
1667
|
}
|
|
673
1668
|
const notice = statusNotice(doc, state.phase, state.error, state.home, () => void load());
|
|
674
1669
|
if (notice) wrapper.append(notice);
|
|
675
|
-
if (state.phase === 'loading' || state.phase === 'error' || state.home?.status === 'unavailable' || state.home?.status === 'invalid') {
|
|
1670
|
+
if (state.phase === 'loading' || state.phase === 'error' || state.home?.status === 'unavailable' || state.home?.status === 'error' || state.home?.status === 'invalid') {
|
|
676
1671
|
wrapper.append(workspaceDetailEmpty(doc, { mark: '…', title: state.phase === 'loading' ? '判断履歴を読み込んでいます' : '判断履歴を確認できません', text: '再試行すると最新の状態を確認します。' }));
|
|
677
1672
|
root.replaceChildren(wrapper);
|
|
678
|
-
renderRail(doc, rail, null);
|
|
1673
|
+
renderRail(doc, rail, null, null);
|
|
679
1674
|
return;
|
|
680
1675
|
}
|
|
681
1676
|
const controls = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-controls' });
|
|
682
1677
|
controls.append(periodButtons(doc, state.period, (period) => {
|
|
683
1678
|
state.period = period;
|
|
684
1679
|
state.selectedKey = null;
|
|
685
|
-
state.
|
|
686
|
-
|
|
1680
|
+
state.cursor = null;
|
|
1681
|
+
if (normalMode()) void load({ append: false });
|
|
1682
|
+
else {
|
|
1683
|
+
state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
|
|
1684
|
+
render();
|
|
1685
|
+
}
|
|
687
1686
|
}));
|
|
1687
|
+
if (normalMode()) controls.append(normalFilterControls(doc, state.home, state, handleNormalFilterChange));
|
|
688
1688
|
controls.append(createSearch(doc, state.query, (query) => {
|
|
689
1689
|
state.query = String(query);
|
|
690
1690
|
state.focusSearch = true;
|
|
@@ -699,20 +1699,26 @@ export function createJudgmentHistoryUI({
|
|
|
699
1699
|
attrs: { 'aria-label': '判断履歴を再読み込み', 'data-action': 'reload' },
|
|
700
1700
|
}));
|
|
701
1701
|
wrapper.append(controls);
|
|
1702
|
+
const normalSourceSummary = normalSourcesSummary(state.home);
|
|
702
1703
|
wrapper.append(makeWorkspaceElement(doc, 'p', {
|
|
703
1704
|
className: 'bb-jh-reference-note',
|
|
704
|
-
text:
|
|
1705
|
+
text: normalMode()
|
|
1706
|
+
? `保存された判断の理由、当時の参照、案、実行と結果を表示しています。参照の本文は現在の権限で確認できる場合だけ開きます。${normalSourceSummary ? ` 取得元: ${normalSourceSummary}` : ''}`
|
|
1707
|
+
: '判断に残った参照だけを表示しています。実行結果の証拠は参照件数に含めません。',
|
|
705
1708
|
}));
|
|
706
|
-
if (state.home?.status === 'partial') wrapper.append(statusNotice(doc, 'ready', null, state.home, () => void load()));
|
|
707
1709
|
const stats = aggregate.stats ?? {};
|
|
708
1710
|
const metricCount = (value, suffix) => value === null || value === undefined ? '未確認' : `${value}${suffix}`;
|
|
709
|
-
wrapper.append(workspaceMetrics(doc, [
|
|
1711
|
+
wrapper.append(workspaceMetrics(doc, normalMode() ? [
|
|
1712
|
+
{ label: '判断件数', value: metricCount(stats.judgments, '件'), note: 'この期間・絞り込みの履歴' },
|
|
1713
|
+
{ label: '使った参照', value: metricCount(stats.references, '件'), note: '当時の記録にある一意の参照' },
|
|
1714
|
+
{ label: '確認済みの結果', value: metricCount(stats.confirmedOutcomes, '件'), note: '成果が確認済みと記録された判断' },
|
|
1715
|
+
] : [
|
|
710
1716
|
{ label: 'Brainbaseが代わりに判断した件数', value: metricCount(stats.judgments, '件'), note: 'この期間の履歴' },
|
|
711
1717
|
{ label: 'そこで使った参照', value: metricCount(stats.references, '件'), note: '代理判断に含まれる一意の参照' },
|
|
712
1718
|
{ label: '判断回数にすると', value: metricCount(stats.equivalent, '回分相当'), note: '代理判断1件を1回分として表示' },
|
|
713
1719
|
], { ariaLabel: '判断履歴の集計' }));
|
|
714
1720
|
const emptyText = state.home?.status === 'partial'
|
|
715
|
-
? '
|
|
1721
|
+
? 'この期間に取得できた判断履歴はありません。未接続・一部取得のため0件とは確認できません。'
|
|
716
1722
|
: 'この期間の判断履歴はありません。';
|
|
717
1723
|
wrapper.append(workspaceLedger(doc, {
|
|
718
1724
|
className: 'bb-jh-ledger',
|
|
@@ -721,10 +1727,21 @@ export function createJudgmentHistoryUI({
|
|
|
721
1727
|
rows: makeRows(doc, aggregate, state.selectedKey, (key) => {
|
|
722
1728
|
state.lastFocusedKey = key;
|
|
723
1729
|
state.selectedKey = key;
|
|
1730
|
+
state.detailPhase = 'idle';
|
|
1731
|
+
state.detailError = null;
|
|
724
1732
|
render();
|
|
1733
|
+
const selectedRow = aggregate.rows.find((row) => row.key === key);
|
|
1734
|
+
if (selectedRow?.mode === 'normal') void loadDetail(selectedRow);
|
|
725
1735
|
}),
|
|
726
1736
|
empty: emptyText,
|
|
727
1737
|
}));
|
|
1738
|
+
const pagination = paginationControls(doc, aggregate, () => {
|
|
1739
|
+
if (aggregate.pagination?.nextCursor) {
|
|
1740
|
+
state.cursor = aggregate.pagination.nextCursor;
|
|
1741
|
+
void load({ cursor: state.cursor, append: true });
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
if (pagination) wrapper.append(pagination);
|
|
728
1745
|
const nextSearchInput = findFirst(wrapper, (node) => node.tagName === 'INPUT');
|
|
729
1746
|
if (previousSearchInput && nextSearchInput) {
|
|
730
1747
|
previousSearchInput.value = state.query;
|
|
@@ -740,7 +1757,23 @@ export function createJudgmentHistoryUI({
|
|
|
740
1757
|
state.focusSearch = false;
|
|
741
1758
|
}
|
|
742
1759
|
const selected = aggregate.rows.find((row) => row.key === state.selectedKey) ?? null;
|
|
743
|
-
|
|
1760
|
+
const selectedDetail = selected ? state.details.get(selected.recordId) ?? null : null;
|
|
1761
|
+
const railOptions = selected?.mode === 'normal'
|
|
1762
|
+
? {
|
|
1763
|
+
detailReady: state.detailPhase === 'ready' && Boolean(selectedDetail),
|
|
1764
|
+
detailPhase: state.detailPhase,
|
|
1765
|
+
draft: feedbackDraft(selected.recordId),
|
|
1766
|
+
onKindChange: (kind) => {
|
|
1767
|
+
updateFeedbackDraft(selected.recordId, { kind: feedbackKind(kind) ?? 'feedback' });
|
|
1768
|
+
render();
|
|
1769
|
+
},
|
|
1770
|
+
onSummaryChange: (summary) => updateFeedbackDraft(selected.recordId, { summary: String(summary ?? '') }),
|
|
1771
|
+
onOutcomeStatusChange: (outcomeStatus) => updateFeedbackDraft(selected.recordId, { outcomeStatus: String(outcomeStatus ?? '') }),
|
|
1772
|
+
onSubmitFeedback: (controls) => void submitFeedback(selected, controls),
|
|
1773
|
+
onCopyReference: (payload) => void copyReference(payload),
|
|
1774
|
+
}
|
|
1775
|
+
: {};
|
|
1776
|
+
renderRail(doc, rail, selected, selectedDetail, selected ? closeSelected : null, railOptions);
|
|
744
1777
|
if (state.lastFocusedKey) {
|
|
745
1778
|
const row = findFirst(root, (node) => attributeValue(node, 'data-key') === state.lastFocusedKey);
|
|
746
1779
|
row?.focus?.();
|
|
@@ -748,13 +1781,15 @@ export function createJudgmentHistoryUI({
|
|
|
748
1781
|
}
|
|
749
1782
|
}
|
|
750
1783
|
|
|
751
|
-
async function load() {
|
|
1784
|
+
async function load({ cursor = null, append = false } = {}) {
|
|
752
1785
|
if (state.disposed) return null;
|
|
753
1786
|
const generation = ++state.generation;
|
|
754
1787
|
state.phase = 'loading';
|
|
755
1788
|
state.error = null;
|
|
756
|
-
|
|
757
|
-
|
|
1789
|
+
if (!append) {
|
|
1790
|
+
state.home = null;
|
|
1791
|
+
state.aggregate = null;
|
|
1792
|
+
}
|
|
758
1793
|
render();
|
|
759
1794
|
if (!request) {
|
|
760
1795
|
if (generation !== state.generation || state.disposed) return null;
|
|
@@ -764,19 +1799,45 @@ export function createJudgmentHistoryUI({
|
|
|
764
1799
|
return null;
|
|
765
1800
|
}
|
|
766
1801
|
try {
|
|
767
|
-
|
|
1802
|
+
let activePath = normalizedBasePath;
|
|
1803
|
+
let response = await request(historyPath(normalizedBasePath, cursor), {
|
|
768
1804
|
method: 'GET',
|
|
769
1805
|
headers: token ? { 'x-brainbase-review-token': token } : undefined,
|
|
770
1806
|
});
|
|
771
1807
|
if (generation !== state.generation || state.disposed) return null;
|
|
1808
|
+
if (response?.status === 404 && legacyBasePath && !append && !state.project && !state.entrypoint && !state.cursor) {
|
|
1809
|
+
activePath = legacyBasePath;
|
|
1810
|
+
response = await request(`${legacyBasePath}/home`, {
|
|
1811
|
+
method: 'GET',
|
|
1812
|
+
headers: token ? { 'x-brainbase-review-token': token } : undefined,
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
772
1815
|
if (!response || response.ok !== true) throw new Error(`http_${response?.status ?? 0}`);
|
|
773
1816
|
const payload = await response.json();
|
|
774
1817
|
if (generation !== state.generation || state.disposed) return null;
|
|
775
|
-
|
|
1818
|
+
const normalized = normalizeJudgmentHistoryHome(payload);
|
|
1819
|
+
state.activeBasePath = activePath;
|
|
1820
|
+
if (append && state.home?.mode === 'normal' && normalized.mode === 'normal') {
|
|
1821
|
+
const records = [...state.home.records];
|
|
1822
|
+
const knownRecordIds = new Set(records.map((record) => record.recordId));
|
|
1823
|
+
for (const record of normalized.records) {
|
|
1824
|
+
if (knownRecordIds.has(record.recordId)) continue;
|
|
1825
|
+
knownRecordIds.add(record.recordId);
|
|
1826
|
+
records.push(record);
|
|
1827
|
+
}
|
|
1828
|
+
state.home = { ...normalized, records, coverage: normalized.coverage ?? state.home.coverage };
|
|
1829
|
+
} else {
|
|
1830
|
+
state.home = normalized;
|
|
1831
|
+
}
|
|
1832
|
+
if (state.home.mode === 'normal') {
|
|
1833
|
+
state.project = state.project ?? state.home.filters?.project ?? null;
|
|
1834
|
+
state.entrypoint = state.entrypoint ?? state.home.filters?.entrypoint ?? null;
|
|
1835
|
+
state.cursor = state.home.pagination?.nextCursor ?? null;
|
|
1836
|
+
}
|
|
776
1837
|
state.phase = 'ready';
|
|
777
1838
|
state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
|
|
778
1839
|
if (state.pendingDecisionId) {
|
|
779
|
-
const pending = state.aggregate.rows.find((row) => row.decisionAttemptId === state.pendingDecisionId);
|
|
1840
|
+
const pending = state.aggregate.rows.find((row) => row.decisionAttemptId === state.pendingDecisionId || row.recordId === state.pendingDecisionId);
|
|
780
1841
|
state.pendingDecisionId = null;
|
|
781
1842
|
if (pending) state.selectedKey = pending.key;
|
|
782
1843
|
}
|
|
@@ -798,11 +1859,12 @@ export function createJudgmentHistoryUI({
|
|
|
798
1859
|
state.pendingDecisionId = id;
|
|
799
1860
|
return true;
|
|
800
1861
|
}
|
|
801
|
-
const row = state.aggregate?.rows?.find((candidate) => candidate.decisionAttemptId === id);
|
|
1862
|
+
const row = state.aggregate?.rows?.find((candidate) => candidate.decisionAttemptId === id || candidate.recordId === id);
|
|
802
1863
|
if (!row) return false;
|
|
803
1864
|
state.selectedKey = row.key;
|
|
804
1865
|
state.lastFocusedKey = row.key;
|
|
805
1866
|
render();
|
|
1867
|
+
if (row.mode === 'normal') void loadDetail(row);
|
|
806
1868
|
return true;
|
|
807
1869
|
}
|
|
808
1870
|
|