@unson/brainbase-mcp 0.15.0 → 0.15.2
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/canonical-task-service.d.ts +11 -7
- package/dist/canonical-task-service.js +49 -21
- package/dist/cli.js +39 -3
- package/dist/graph-web-http.d.ts +7 -0
- package/dist/graph-web-http.js +14 -0
- package/dist/graph-web.d.ts +36 -6
- package/dist/graph-web.js +45 -4
- package/dist/judgment-value-proof-review.d.ts +76 -0
- package/dist/judgment-value-proof-review.js +294 -10
- package/dist/knowledge-continuation.js +18 -5
- package/dist/knowledge-lookup.js +7 -0
- package/dist/local-web-host.d.ts +23 -0
- package/dist/local-web-host.js +26 -7
- package/dist/organization-graph-web.d.ts +74 -0
- package/dist/organization-graph-web.js +275 -0
- package/dist/value-proof-review-http.d.ts +4 -1
- package/dist/value-proof-review-http.js +76 -12
- package/dist/world-extension.d.ts +148 -0
- package/dist/world-extension.js +277 -0
- package/package.json +5 -3
- package/ui/graph-projects-view.js +6 -3
- package/ui/graph-registry-view.js +6 -3
- package/ui/graph-view-shared.css +2 -0
- package/ui/graph-view-shared.js +9 -3
- package/ui/local-web-shell.js +82 -11
- package/ui/value-proof-review.css +24 -0
- package/ui/value-proof-review.js +241 -12
- package/ui/world/world-placement.js +45 -0
- package/ui/world/world-vendor.LICENSE.txt +23 -0
- package/ui/world/world-vendor.js +4220 -0
- package/ui/world/world-view.css +234 -0
- package/ui/world/world-view.js +1033 -0
package/ui/local-web-shell.js
CHANGED
|
@@ -43,6 +43,8 @@ const NAV_ICON_PATHS = Object.freeze({
|
|
|
43
43
|
objectives: ['M12 4 3 20h18z', 'M12 9v5m0 3h.01'],
|
|
44
44
|
projects: ['M3 6.5h7l2 2h9v10H3z', 'M3 6.5v-2h7l2 2'],
|
|
45
45
|
graph: ['M10 13a5 5 0 0 0 7.5.5l2-2a5 5 0 0 0-7-7l-1.2 1.2', 'M14 11a5 5 0 0 0-7.5-.5l-2 2a5 5 0 0 0 7 7l1.2-1.2'],
|
|
46
|
+
// World view (extension id `world`, ledger P17).
|
|
47
|
+
world: ['M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z', 'M3 12h18', 'M12 3c2.5 2.6 3.8 5.6 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.6-3.8-9S9.5 5.6 12 3z'],
|
|
46
48
|
});
|
|
47
49
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
48
50
|
|
|
@@ -88,9 +90,15 @@ export function parseLocalWebTarget(target) {
|
|
|
88
90
|
if (!screenId) return { screenId: null, entityId: null };
|
|
89
91
|
const query = separator < 0 ? '' : raw.slice(separator + 1);
|
|
90
92
|
const params = new URLSearchParams(query);
|
|
93
|
+
// 今日 accepts one judgment to open (`decision` = decision_attempt_id) and
|
|
94
|
+
// プロジェクトと関係者 one project (`project` = Graph project id); other screens ignore them.
|
|
95
|
+
const decisionId = screenId === 'today' ? params.get('decision')?.trim() || null : null;
|
|
96
|
+
const projectId = screenId === 'projects' ? params.get('project')?.trim() || null : null;
|
|
91
97
|
return {
|
|
92
98
|
screenId,
|
|
93
99
|
entityId: screenId === 'graph' ? params.get('entity_id')?.trim() || null : null,
|
|
100
|
+
...(decisionId ? { decisionId } : {}),
|
|
101
|
+
...(projectId ? { projectId } : {}),
|
|
94
102
|
};
|
|
95
103
|
}
|
|
96
104
|
|
|
@@ -195,11 +203,67 @@ function mountObjectives(container, context) {
|
|
|
195
203
|
return { editor, worldModel };
|
|
196
204
|
}
|
|
197
205
|
|
|
198
|
-
|
|
206
|
+
const ORGANIZATION_READ_ONLY_NOTE = '組織のGraphを読み取り専用で表示しています。登録の訂正は組織版で行ってください。';
|
|
207
|
+
// What the owner needs before leaving for the organization web (Inspector UX-20261004-01): a link to this
|
|
208
|
+
// very record there, who may correct it, and what happens after saving. The organization web opens
|
|
209
|
+
// 情報と関係 by record id and プロジェクトと関係者 by project code, and keeps that record across sign-in.
|
|
210
|
+
const ORGANIZATION_RECORD_ID = /^[A-Za-z0-9_.:-]{1,200}$/u;
|
|
211
|
+
export function organizationRecordLink(origin, screenId, record) {
|
|
212
|
+
const id = typeof record?.id === 'string' && ORGANIZATION_RECORD_ID.test(record.id) ? record.id : null;
|
|
213
|
+
// The organization web picks a project by its own code. Only that code is used: a sub-project's scope
|
|
214
|
+
// code is its parent's and would open the parent, so without its own code the record opens by id.
|
|
215
|
+
const own = typeof record?.metadata?.code === 'string' ? record.metadata.code.trim() : '';
|
|
216
|
+
const code = own && ORGANIZATION_RECORD_ID.test(own) ? own : null;
|
|
217
|
+
if (screenId === 'projects' && record?.type === 'project' && code) {
|
|
218
|
+
return { href: `${origin}/?screen=projects&project=${encodeURIComponent(code)}`, label: '「プロジェクトと関係者」' };
|
|
219
|
+
}
|
|
220
|
+
return id ? { href: `${origin}/?screen=graph&entity_id=${encodeURIComponent(id)}`, label: '「情報と関係」' } : null;
|
|
221
|
+
}
|
|
222
|
+
const organizationReadOnlyNoteLinked = (doc, origin, screenId) => (record) => {
|
|
223
|
+
const target = organizationRecordLink(origin, screenId, record);
|
|
224
|
+
if (!target) return ORGANIZATION_READ_ONLY_NOTE;
|
|
225
|
+
const note = makeElement(doc, 'span');
|
|
226
|
+
note.append(
|
|
227
|
+
makeElement(doc, 'span', { text: [
|
|
228
|
+
'組織のGraphを読み取り専用で表示しています。ここでは直せません。訂正の手順:',
|
|
229
|
+
`① 下のリンクから、組織版の${target.label}でこの記録を開きます。ログインしていなければSlackでログインすると、そのままこの記録に戻ります。`,
|
|
230
|
+
'② 直せるのは、組織版での役割がgmかceoで、このプロジェクトとその上の事業の両方を許可されている人です。直せるのはプロジェクトの記録(名前・別名・要約・期間・目的・状態)だけで、人物の記録と、関係者の追加・終了は組織版でもまだ直せません。',
|
|
231
|
+
'③ 保存すると、理由といっしょに組織のGraphへ書き込まれ、組織版がその場で読み戻して表示します。この画面には、1分ほどたってから再読み込みすると反映されます。',
|
|
232
|
+
'',
|
|
233
|
+
].join('\n') }),
|
|
234
|
+
makeElement(doc, 'a', { text: '組織版でこの記録を開く', attrs: { href: target.href, target: '_blank', rel: 'noopener noreferrer' } }),
|
|
235
|
+
);
|
|
236
|
+
return note;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/** The organization's web for this screen, or null when the host was not told where it is. */
|
|
240
|
+
function organizationWebLink(doc, organizationGraph, screenId) {
|
|
241
|
+
const origin = typeof organizationGraph?.web_url === 'string' && /^https:\/\/[^/?#@\s]+$/u.test(organizationGraph.web_url)
|
|
242
|
+
? organizationGraph.web_url
|
|
243
|
+
: null;
|
|
244
|
+
if (!origin) return null;
|
|
245
|
+
return makeElement(doc, 'a', {
|
|
246
|
+
text: '組織版で開いて訂正する',
|
|
247
|
+
attrs: { href: `${origin}/?screen=${encodeURIComponent(screenId)}`, target: '_blank', rel: 'noopener noreferrer' },
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function mountGraphScreen(screenId, createView) {
|
|
199
252
|
return (container, context) => {
|
|
200
253
|
const viewRoot = makeElement(context.document, 'div');
|
|
201
254
|
makePage(container, context).append(viewRoot);
|
|
255
|
+
// Ledger C1: when the host reads the organization Graph, nothing here can be corrected.
|
|
256
|
+
const readOnly = context.organizationGraph?.status === 'connected';
|
|
257
|
+
const link = readOnly ? organizationWebLink(context.document, context.organizationGraph, screenId) : null;
|
|
258
|
+
const sourceText = `組織のGraph(${context.organizationGraph?.server ?? '接続先不明'})を読み取り専用で表示しています。このMacのGraphではありません。`;
|
|
259
|
+
const source = link ? makeElement(context.document, 'span') : null;
|
|
260
|
+
source?.append(makeElement(context.document, 'span', { text: `${sourceText} ` }), link);
|
|
202
261
|
return createView({
|
|
262
|
+
...(readOnly ? {
|
|
263
|
+
canCorrect: false,
|
|
264
|
+
readOnlyNote: link ? organizationReadOnlyNoteLinked(context.document, context.organizationGraph.web_url, screenId) : ORGANIZATION_READ_ONLY_NOTE,
|
|
265
|
+
sourceNotice: { label: '出典', text: source ?? sourceText },
|
|
266
|
+
} : {}),
|
|
203
267
|
root: viewRoot,
|
|
204
268
|
rail: context.rail,
|
|
205
269
|
page: context.page,
|
|
@@ -214,8 +278,8 @@ function mountGraphScreen(createView) {
|
|
|
214
278
|
export const LOCAL_WEB_SCREENS = Object.freeze([
|
|
215
279
|
Object.freeze({ id: 'today', label: '今日', usesGraph: false, rail: true, source: '判断journal', mount: mountToday }),
|
|
216
280
|
Object.freeze({ id: 'objectives', label: '目的と現状', usesGraph: true, rail: true, source: '手元のGraph', mount: mountObjectives }),
|
|
217
|
-
Object.freeze({ id: 'projects', label: 'プロジェクトと関係者', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen(createGraphProjectsView) }),
|
|
218
|
-
Object.freeze({ id: 'graph', label: '情報と関係', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen(createGraphRegistryView) }),
|
|
281
|
+
Object.freeze({ id: 'projects', label: 'プロジェクトと関係者', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('projects', createGraphProjectsView) }),
|
|
282
|
+
Object.freeze({ id: 'graph', label: '情報と関係', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('graph', createGraphRegistryView) }),
|
|
219
283
|
]);
|
|
220
284
|
|
|
221
285
|
function renderGraphGate(doc, slot, status, onRecheck, label) {
|
|
@@ -279,7 +343,7 @@ function renderSource(doc, bar, status, onRecheck) {
|
|
|
279
343
|
bar.append(
|
|
280
344
|
fact('データ', data.data_dir),
|
|
281
345
|
fact('Graph', graphLabel, data.graph.status === 'ready' ? '' : 'is-warning'),
|
|
282
|
-
fact('組織のGraph', data.organization_graph?.status === 'connected' ? '
|
|
346
|
+
fact('組織のGraph', data.organization_graph?.status === 'connected' ? '読んでいます(読み取りのみ)' : '読んでいません'),
|
|
283
347
|
);
|
|
284
348
|
}
|
|
285
349
|
|
|
@@ -377,10 +441,10 @@ export function createLocalWebShell({
|
|
|
377
441
|
|
|
378
442
|
function ensureMounted(screen, entityId = pendingEntityIds.get(screen.id) ?? null) {
|
|
379
443
|
if (mounted.has(screen.id)) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
444
|
+
const view = mounted.get(screen.id);
|
|
445
|
+
if (entityId && screen.id === 'graph' && typeof view?.openEntity === 'function') void view.openEntity(entityId);
|
|
446
|
+
if (entityId && screen.id === 'today' && typeof view?.openDecision === 'function') view.openDecision(entityId);
|
|
447
|
+
if (entityId && screen.id === 'projects' && typeof view?.openProject === 'function') void view.openProject(entityId);
|
|
384
448
|
return;
|
|
385
449
|
}
|
|
386
450
|
const slot = slots.get(screen.id);
|
|
@@ -391,8 +455,13 @@ export function createLocalWebShell({
|
|
|
391
455
|
slot.replaceChildren();
|
|
392
456
|
const rail = rails.get(screen.id) ?? null;
|
|
393
457
|
rail?.replaceChildren();
|
|
394
|
-
const
|
|
395
|
-
|
|
458
|
+
const organizationGraph = status.phase === 'ready' ? status.data.organization_graph ?? null : null;
|
|
459
|
+
const readsOrganization = organizationGraph?.status === 'connected' && (screen.id === 'projects' || screen.id === 'graph');
|
|
460
|
+
const page = Object.freeze({ crumbs: Object.freeze(['あなたのBrainbase', screen.label]), source: readsOrganization ? '組織のGraph(読み取りのみ)' : screen.source ?? null });
|
|
461
|
+
const view = screen.mount(slot, Object.freeze({ ...context, rail, page, initialEntityId: entityId, organizationGraph })) ?? true;
|
|
462
|
+
mounted.set(screen.id, view);
|
|
463
|
+
if (entityId && screen.id === 'today' && typeof view?.openDecision === 'function') view.openDecision(entityId);
|
|
464
|
+
if (entityId && screen.id === 'projects' && typeof view?.openProject === 'function') void view.openProject(entityId);
|
|
396
465
|
updateRail();
|
|
397
466
|
}
|
|
398
467
|
|
|
@@ -403,7 +472,9 @@ export function createLocalWebShell({
|
|
|
403
472
|
show(target) {
|
|
404
473
|
const parsed = parseLocalWebTarget(target);
|
|
405
474
|
const screen = screens.find((candidate) => candidate.id === parsed.screenId) ?? screens[0];
|
|
406
|
-
pendingEntityIds.set(screen.id, screen.id === 'graph' ? parsed.entityId
|
|
475
|
+
pendingEntityIds.set(screen.id, screen.id === 'graph' ? parsed.entityId
|
|
476
|
+
: screen.id === 'today' ? parsed.decisionId ?? null
|
|
477
|
+
: screen.id === 'projects' ? parsed.projectId ?? null : null);
|
|
407
478
|
active = screen.id;
|
|
408
479
|
for (const [screenId, slot] of slots) slot.hidden = screenId !== screen.id;
|
|
409
480
|
for (const [screenId, link] of links) {
|
|
@@ -503,6 +503,30 @@ body:has(#brainbase-value-proof-review) {
|
|
|
503
503
|
outline-offset: 2px;
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
/* The answer area of a judgment returned to the owner, and its confirmation. */
|
|
507
|
+
.bb-vpr-answer {
|
|
508
|
+
display: grid;
|
|
509
|
+
gap: var(--bb-space-2);
|
|
510
|
+
margin-top: var(--bb-space-3);
|
|
511
|
+
padding: var(--bb-space-3);
|
|
512
|
+
border: 1px solid var(--vpr-border-strong);
|
|
513
|
+
border-radius: var(--bb-radius-sm, 7px);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.bb-vpr-answer h4,
|
|
517
|
+
.bb-vpr-answer .bb-vpr-hint {
|
|
518
|
+
margin: 0;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.bb-vpr-answer-confirm {
|
|
522
|
+
margin: 0;
|
|
523
|
+
padding: 11px 12px;
|
|
524
|
+
border-radius: var(--bb-radius-sm, 7px);
|
|
525
|
+
color: var(--vpr-ink);
|
|
526
|
+
background: var(--vpr-warning-soft);
|
|
527
|
+
font-size: 13px;
|
|
528
|
+
}
|
|
529
|
+
|
|
506
530
|
/* Messages under the form and the consult button. */
|
|
507
531
|
.vpr-notice {
|
|
508
532
|
margin: 0;
|
package/ui/value-proof-review.js
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
workspaceSectionTitle,
|
|
30
30
|
} from './workspace-kit.js';
|
|
31
31
|
|
|
32
|
-
export const VALUE_PROOF_REVIEW_UI_CONTRACT_VERSION = 'value-proof-review-ui.
|
|
32
|
+
export const VALUE_PROOF_REVIEW_UI_CONTRACT_VERSION = 'value-proof-review-ui.v3';
|
|
33
33
|
|
|
34
34
|
const SECTION_ORDER = Object.freeze(['needs_human', 'blocked', 'continued']);
|
|
35
35
|
const SECTION_LABELS = Object.freeze({
|
|
@@ -80,6 +80,16 @@ const FEEDBACK_LAYER_LABELS = Object.freeze({
|
|
|
80
80
|
philosophy: '大切にすること',
|
|
81
81
|
other: 'その他',
|
|
82
82
|
});
|
|
83
|
+
/** Shown when the host does not offer recording answers (no `capabilities.answer` in the home). */
|
|
84
|
+
const HUMAN_DECISION_BOUNDARY_NOTE = 'この画面では、人に戻した判断への回答や作業の再開はできません。';
|
|
85
|
+
const ANSWER_HINT = 'この画面で、選択肢を選んで回答を記録できます。記録しても、止まった作業は自動では再開しません。';
|
|
86
|
+
const ANSWER_FINAL_NOTE = '記録した回答は、この画面では変更・取り消しできません。';
|
|
87
|
+
const ANSWER_RESUME_NOTE = '止まった作業は自動では再開しません。続きを進めるときは、AIにこの回答を伝えて頼み直してください。';
|
|
88
|
+
const ANSWER_REJECT_LABEL = 'どれも選ばない';
|
|
89
|
+
const ANSWER_KINDS = Object.freeze(['select', 'reject']);
|
|
90
|
+
const MOVED_ON_LABEL = '会話で先に進んだ';
|
|
91
|
+
const FEEDBACK_PURPOSE_NOTE = '評価は、実行後の振り返りとして記録します。人に戻した判断への回答や承認ではありません。';
|
|
92
|
+
const CONSULT_COPY_NOTE = '相談文をコピーするだけで、この画面からCodexへは送信しません。';
|
|
83
93
|
/** Layers the owner can pick for a correction. `delegation` comes from 「次回は聞く」. */
|
|
84
94
|
export const VALUE_PROOF_CORRECTION_LAYERS = Object.freeze(['method', 'objective', 'world_model', 'philosophy', 'other']);
|
|
85
95
|
export const VALUE_PROOF_FEEDBACK_OPTIONS = Object.freeze([
|
|
@@ -138,6 +148,40 @@ function isProof(value) {
|
|
|
138
148
|
&& isRecord(value.feedback);
|
|
139
149
|
}
|
|
140
150
|
|
|
151
|
+
/** The recorded answer, or null. A malformed answer is treated as absent, never as answered. */
|
|
152
|
+
function normalizeAnswer(answer) {
|
|
153
|
+
if (!isRecord(answer) || !ANSWER_KINDS.includes(answer.kind) || !text(answer.recorded_at)) return null;
|
|
154
|
+
if (answer.kind === 'select' && !text(answer.selected_option_id)) return null;
|
|
155
|
+
return {
|
|
156
|
+
kind: answer.kind,
|
|
157
|
+
optionId: answer.kind === 'select' ? text(answer.selected_option_id) : null,
|
|
158
|
+
optionLabel: answer.kind === 'select' ? text(answer.selected_option_label) : null,
|
|
159
|
+
recordedAt: text(answer.recorded_at),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** When the conversation moved on after the question, or null. */
|
|
164
|
+
function normalizeMovedOnAt(value) {
|
|
165
|
+
const at = text(value);
|
|
166
|
+
return at && !Number.isNaN(Date.parse(at)) ? at : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A judgment returned to the owner that still has no recorded answer and whose conversation is still at the question. */
|
|
170
|
+
function awaitsOwner(item) {
|
|
171
|
+
return item.section === 'needs_human' && !item.answer && !item.conversationMovedOnAt;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The section label, or how the owner's side of a returned judgment ended. */
|
|
175
|
+
function itemStateLabel(item) {
|
|
176
|
+
if (item.answer) return '回答済み';
|
|
177
|
+
return item.conversationMovedOnAt ? MOVED_ON_LABEL : SECTION_LABELS[item.section];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Shown instead of the answer controls: the owner answered in that conversation or it moved on. */
|
|
181
|
+
function movedOnNote(at) {
|
|
182
|
+
return `この問いのあと、同じ会話で次のやり取りがありました(${formatDate(at)})。会話の中で答えたか、話が先に進んだと考えられるので、この画面では回答を受け付けません。続きが必要なら、元の会話で頼んでください。`;
|
|
183
|
+
}
|
|
184
|
+
|
|
141
185
|
/** Normalize the host response. Anything unexpected becomes `invalid`, never an empty success. */
|
|
142
186
|
export function normalizeValueProofReviewHome(payload) {
|
|
143
187
|
if (!isRecord(payload)) return { status: 'invalid', reason: '応答の形式が不正です' };
|
|
@@ -157,6 +201,8 @@ export function normalizeValueProofReviewHome(payload) {
|
|
|
157
201
|
section,
|
|
158
202
|
proof: item.proof,
|
|
159
203
|
feedbackHistory: Array.isArray(item.feedback_history) ? item.feedback_history.filter(isRecord) : [],
|
|
204
|
+
answer: normalizeAnswer(item.answer),
|
|
205
|
+
conversationMovedOnAt: normalizeMovedOnAt(item.conversation_moved_on_at),
|
|
160
206
|
}));
|
|
161
207
|
}
|
|
162
208
|
const itemsByKey = new Map(SECTION_ORDER.flatMap((section) => sections[section].map((item) => [itemKey(item.proof), item])));
|
|
@@ -175,6 +221,8 @@ export function normalizeValueProofReviewHome(payload) {
|
|
|
175
221
|
itemsByKey,
|
|
176
222
|
delegationMap,
|
|
177
223
|
rejected: Array.isArray(payload.rejected) ? payload.rejected.filter(isRecord) : [],
|
|
224
|
+
// Only a host that records answers says so; elsewhere the screen keeps the 回答できません note.
|
|
225
|
+
answersRecordable: isRecord(payload.capabilities) && payload.capabilities.answer === 'record_only',
|
|
178
226
|
};
|
|
179
227
|
}
|
|
180
228
|
|
|
@@ -251,7 +299,7 @@ function isUnrated(proof) {
|
|
|
251
299
|
function itemVisible(state, item) {
|
|
252
300
|
if (!item) return false;
|
|
253
301
|
if (state.unratedOnly && !isUnrated(item.proof)) return false;
|
|
254
|
-
if (state.needsHumanOnly && item
|
|
302
|
+
if (state.needsHumanOnly && !awaitsOwner(item)) return false;
|
|
255
303
|
return true;
|
|
256
304
|
}
|
|
257
305
|
|
|
@@ -268,6 +316,15 @@ function rowOfItem(state, key) {
|
|
|
268
316
|
return state.home?.delegationMap?.rows.find((row) => row.itemKeys.includes(key)) ?? null;
|
|
269
317
|
}
|
|
270
318
|
|
|
319
|
+
function findItemByDecision(state, decisionAttemptId) {
|
|
320
|
+
if (state.home?.status !== 'available' || !decisionAttemptId) return null;
|
|
321
|
+
for (const section of [...SECTION_ORDER, 'other']) {
|
|
322
|
+
const found = state.home.sections[section].find((item) => item.proof.decision_attempt_id === decisionAttemptId);
|
|
323
|
+
if (found) return found;
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
271
328
|
function findItem(state, key) {
|
|
272
329
|
if (state.home?.status !== 'available' || !key) return null;
|
|
273
330
|
for (const section of [...SECTION_ORDER, 'other']) {
|
|
@@ -370,7 +427,7 @@ function toggleButton(doc, { label, pressed, disabled = false, onClick }) {
|
|
|
370
427
|
}
|
|
371
428
|
|
|
372
429
|
function renderFilters(doc, home, state, callbacks) {
|
|
373
|
-
const needsHuman = home.sections.needs_human.length;
|
|
430
|
+
const needsHuman = home.sections.needs_human.filter(awaitsOwner).length;
|
|
374
431
|
const actions = workspaceActions(doc, [
|
|
375
432
|
toggleButton(doc, {
|
|
376
433
|
label: state.needsHumanOnly ? `あなたの判断が必要 ${needsHuman}件(絞り込み中・解除)` : `あなたの判断が必要 ${needsHuman}件`,
|
|
@@ -391,9 +448,17 @@ function renderFilters(doc, home, state, callbacks) {
|
|
|
391
448
|
|
|
392
449
|
function renderMetrics(doc, home) {
|
|
393
450
|
const map = home.delegationMap;
|
|
451
|
+
const excluded = [
|
|
452
|
+
home.answersRecordable && '回答済み',
|
|
453
|
+
home.sections.needs_human.some((item) => item.conversationMovedOnAt) && '会話で先に進んだもの',
|
|
454
|
+
].filter(Boolean);
|
|
394
455
|
return workspaceMetrics(doc, [
|
|
395
456
|
{ label: '保存済み', value: home.coverage.saved, note: '判断journalの記録' },
|
|
396
|
-
{
|
|
457
|
+
{
|
|
458
|
+
label: 'あなたの判断が必要',
|
|
459
|
+
value: home.sections.needs_human.filter(awaitsOwner).length,
|
|
460
|
+
note: excluded.length > 0 ? `あなたに戻した判断(${excluded.join('と、')}を除く)` : 'あなたに戻した判断',
|
|
461
|
+
},
|
|
397
462
|
{ label: '聞かずに進めた', value: home.sections.continued.length, note: '聞かずに続行した判断' },
|
|
398
463
|
{ label: '評価済み', value: map.rated, note: `判断 ${map.judged}件のうち` },
|
|
399
464
|
], { ariaLabel: '判断の集計' });
|
|
@@ -668,7 +733,7 @@ function renderItemButton(doc, state, item, callbacks) {
|
|
|
668
733
|
makeElement(doc, 'span', {
|
|
669
734
|
className: 'bb-vpr-item-meta',
|
|
670
735
|
text: [
|
|
671
|
-
|
|
736
|
+
itemStateLabel(item),
|
|
672
737
|
formatDate(item.proof.recorded_at),
|
|
673
738
|
OUTCOME_LABELS[item.proof.outcome.status] ?? '成果不明',
|
|
674
739
|
FEEDBACK_LABELS[item.proof.feedback.status] ?? '評価不明',
|
|
@@ -772,6 +837,65 @@ function renderFeedbackForm(doc, item, state, callbacks) {
|
|
|
772
837
|
return form;
|
|
773
838
|
}
|
|
774
839
|
|
|
840
|
+
function answerChoiceText(kind, optionLabel, optionId) {
|
|
841
|
+
return kind === 'reject' ? ANSWER_REJECT_LABEL : optionLabel ?? optionId ?? '選択肢';
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/** The answer area of a judgment returned to the owner: the recorded answer, or the choices and the confirmation. */
|
|
845
|
+
function renderAnswer(doc, item, state, callbacks) {
|
|
846
|
+
const key = itemKey(item.proof);
|
|
847
|
+
const save = state.answerSave?.key === key ? state.answerSave : null;
|
|
848
|
+
const box = makeElement(doc, 'div', {
|
|
849
|
+
className: `bb-vpr-answer${item.answer ? ' is-answered' : ''}`,
|
|
850
|
+
attrs: { role: 'group', 'aria-label': item.answer ? '記録した回答' : 'この問いに回答する' },
|
|
851
|
+
});
|
|
852
|
+
if (item.answer) {
|
|
853
|
+
box.append(
|
|
854
|
+
makeElement(doc, 'h4', { text: '回答済み' }),
|
|
855
|
+
makeElement(doc, 'p', {
|
|
856
|
+
text: `「${answerChoiceText(item.answer.kind, item.answer.optionLabel, item.answer.optionId)}」と回答しました(${formatDate(item.answer.recordedAt)})。`,
|
|
857
|
+
}),
|
|
858
|
+
makeElement(doc, 'p', { className: 'bb-vpr-hint', text: `${ANSWER_RESUME_NOTE}${ANSWER_FINAL_NOTE}` }),
|
|
859
|
+
);
|
|
860
|
+
if (save?.state === 'saved') box.append(notice(doc, 'is-success', save.message));
|
|
861
|
+
if (save?.state === 'error') box.append(notice(doc, 'is-danger', save.message, 'alert'));
|
|
862
|
+
return box;
|
|
863
|
+
}
|
|
864
|
+
box.append(makeElement(doc, 'h4', { text: 'この問いに回答する' }), makeElement(doc, 'p', { className: 'bb-vpr-hint', text: ANSWER_HINT }));
|
|
865
|
+
const draft = state.answerDraft?.key === key ? state.answerDraft : null;
|
|
866
|
+
if (draft) {
|
|
867
|
+
box.append(makeElement(doc, 'p', {
|
|
868
|
+
className: 'bb-vpr-answer-confirm',
|
|
869
|
+
text: `「${answerChoiceText(draft.kind, draft.optionLabel, draft.optionId)}」で回答を記録します。${ANSWER_FINAL_NOTE}止まった作業は自動では再開しません。`,
|
|
870
|
+
}));
|
|
871
|
+
const saving = save?.state === 'saving';
|
|
872
|
+
box.append(workspaceActions(doc, [
|
|
873
|
+
workspaceButton(doc, {
|
|
874
|
+
text: saving ? '記録中…' : '回答を記録する',
|
|
875
|
+
variant: 'primary',
|
|
876
|
+
disabled: saving,
|
|
877
|
+
onClick: () => callbacks.onAnswerConfirm?.(item),
|
|
878
|
+
}),
|
|
879
|
+
workspaceButton(doc, { text: 'やめる', variant: 'quiet', disabled: saving, onClick: () => callbacks.onAnswerCancel?.() }),
|
|
880
|
+
]));
|
|
881
|
+
} else {
|
|
882
|
+
const options = Array.isArray(item.proof.human_decision?.options) ? item.proof.human_decision.options : [];
|
|
883
|
+
box.append(workspaceActions(doc, [
|
|
884
|
+
...options.map((option) => workspaceButton(doc, {
|
|
885
|
+
text: `「${text(option.label) ?? option.id}」で回答する`,
|
|
886
|
+
onClick: () => callbacks.onAnswerDraft?.({ key, kind: 'select', optionId: option.id, optionLabel: text(option.label) }),
|
|
887
|
+
})),
|
|
888
|
+
workspaceButton(doc, {
|
|
889
|
+
text: ANSWER_REJECT_LABEL,
|
|
890
|
+
variant: 'quiet',
|
|
891
|
+
onClick: () => callbacks.onAnswerDraft?.({ key, kind: 'reject', optionId: null, optionLabel: null }),
|
|
892
|
+
}),
|
|
893
|
+
]));
|
|
894
|
+
}
|
|
895
|
+
if (save?.state === 'error') box.append(notice(doc, 'is-danger', save.message, 'alert'));
|
|
896
|
+
return box;
|
|
897
|
+
}
|
|
898
|
+
|
|
775
899
|
function consultText(proof) {
|
|
776
900
|
return [
|
|
777
901
|
'Brainbaseのこの判断について相談したい。',
|
|
@@ -798,7 +922,7 @@ function renderJudgmentDetail(doc, state, item, callbacks) {
|
|
|
798
922
|
detail.push(back);
|
|
799
923
|
}
|
|
800
924
|
const head = workspaceRailHead(doc, {
|
|
801
|
-
kicker:
|
|
925
|
+
kicker: itemStateLabel(item),
|
|
802
926
|
title: itemTitle(proof),
|
|
803
927
|
sub: kindLabel(proof) ? `判断の種類: ${kindLabel(proof)}` : row ? rowName(row) : null,
|
|
804
928
|
});
|
|
@@ -851,10 +975,16 @@ function renderJudgmentDetail(doc, state, item, callbacks) {
|
|
|
851
975
|
for (const option of options) list.append(makeElement(doc, 'li', { text: `${text(option.label) ?? option.id}: ${text(option.impact) ?? '影響の記録なし'}` }));
|
|
852
976
|
content.push(makeElement(doc, 'h4', { text: '選択肢と影響' }), list);
|
|
853
977
|
}
|
|
978
|
+
if (item.conversationMovedOnAt && !item.answer) content.push(notice(doc, 'is-muted', movedOnNote(item.conversationMovedOnAt)));
|
|
979
|
+
else if (state.home?.answersRecordable && item.section === 'needs_human') content.push(renderAnswer(doc, item, state, callbacks));
|
|
980
|
+
else content.push(makeElement(doc, 'p', { className: 'bb-vpr-hint', text: HUMAN_DECISION_BOUNDARY_NOTE }));
|
|
854
981
|
detail.push(workspaceRailBlock(doc, { title: 'あなたに戻した理由', className: 'bb-vpr-human-decision', content }));
|
|
855
982
|
}
|
|
856
983
|
|
|
857
|
-
const feedback = [
|
|
984
|
+
const feedback = [
|
|
985
|
+
makeElement(doc, 'p', { className: 'bb-vpr-hint', text: FEEDBACK_PURPOSE_NOTE }),
|
|
986
|
+
renderFeedbackForm(doc, item, state, callbacks),
|
|
987
|
+
];
|
|
858
988
|
if (item.feedbackHistory.length > 1) {
|
|
859
989
|
const history = makeElement(doc, 'details', { className: 'bb-vpr-history' });
|
|
860
990
|
history.append(makeElement(doc, 'summary', { text: `評価の履歴 ${item.feedbackHistory.length}件` }));
|
|
@@ -867,7 +997,10 @@ function renderJudgmentDetail(doc, state, item, callbacks) {
|
|
|
867
997
|
}
|
|
868
998
|
detail.push(workspaceRailBlock(doc, { title: '評価', className: 'bb-vpr-feedback-block', content: feedback }));
|
|
869
999
|
|
|
870
|
-
const consult = [
|
|
1000
|
+
const consult = [
|
|
1001
|
+
makeElement(doc, 'p', { className: 'bb-vpr-hint', text: CONSULT_COPY_NOTE }),
|
|
1002
|
+
workspaceButton(doc, { text: 'Codexで相談する(依頼文をコピー)', onClick: () => callbacks.onConsult?.(consultText(proof)) }),
|
|
1003
|
+
];
|
|
871
1004
|
if (state.consultMessage) consult.push(notice(doc, 'is-muted', state.consultMessage));
|
|
872
1005
|
detail.push(workspaceRailBlock(doc, { title: 'Codexで相談', content: consult }));
|
|
873
1006
|
|
|
@@ -1052,6 +1185,9 @@ export function createValueProofReviewUI({
|
|
|
1052
1185
|
needsHumanOnly: false,
|
|
1053
1186
|
draft: { status: '', summary: '', targetLayer: '' },
|
|
1054
1187
|
save: { state: 'idle', message: '' },
|
|
1188
|
+
/** The answer the owner picked and is about to confirm: `{ key, kind, optionId, optionLabel }`. */
|
|
1189
|
+
answerDraft: null,
|
|
1190
|
+
answerSave: { state: 'idle', message: '', key: null },
|
|
1055
1191
|
focusCard: false,
|
|
1056
1192
|
consultMessage: '',
|
|
1057
1193
|
sourceReads: new Map(),
|
|
@@ -1083,9 +1219,18 @@ export function createValueProofReviewUI({
|
|
|
1083
1219
|
const resetJudgmentInput = () => {
|
|
1084
1220
|
state.draft = { status: '', summary: '', targetLayer: '' };
|
|
1085
1221
|
state.save = { state: 'idle', message: '' };
|
|
1222
|
+
state.answerDraft = null;
|
|
1223
|
+
state.answerSave = { state: 'idle', message: '', key: null };
|
|
1086
1224
|
state.consultMessage = '';
|
|
1087
1225
|
};
|
|
1088
1226
|
|
|
1227
|
+
/** Keep the detail on this judgment, even when a filter now hides it from the lists. */
|
|
1228
|
+
const showJudgment = (key) => {
|
|
1229
|
+
state.selectedKey = key;
|
|
1230
|
+
state.selectedRowKey = rowOfItem(state, key)?.key ?? state.selectedRowKey;
|
|
1231
|
+
state.railView = 'judgment';
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1089
1234
|
const applyFilter = () => {
|
|
1090
1235
|
const previous = state.selectedKey;
|
|
1091
1236
|
revealSelection(state);
|
|
@@ -1167,10 +1312,7 @@ export function createValueProofReviewUI({
|
|
|
1167
1312
|
const key = itemKey(item.proof);
|
|
1168
1313
|
const saved = findItem(state, key);
|
|
1169
1314
|
if (saved?.proof.feedback.status !== option.value) throw new Error('保存後の読み戻しで評価を確認できません');
|
|
1170
|
-
|
|
1171
|
-
state.selectedKey = key;
|
|
1172
|
-
state.selectedRowKey = rowOfItem(state, key)?.key ?? state.selectedRowKey;
|
|
1173
|
-
state.railView = 'judgment';
|
|
1315
|
+
showJudgment(key);
|
|
1174
1316
|
state.draft = { status: '', summary: '', targetLayer: '' };
|
|
1175
1317
|
state.save = { state: 'saved', message: `「${option.label}」を保存しました(読み戻し済み)。` };
|
|
1176
1318
|
} catch (error) {
|
|
@@ -1178,6 +1320,71 @@ export function createValueProofReviewUI({
|
|
|
1178
1320
|
}
|
|
1179
1321
|
controller.render();
|
|
1180
1322
|
},
|
|
1323
|
+
onAnswerDraft(draft) {
|
|
1324
|
+
state.answerDraft = draft;
|
|
1325
|
+
state.answerSave = { state: 'idle', message: '', key: null };
|
|
1326
|
+
controller.render();
|
|
1327
|
+
},
|
|
1328
|
+
onAnswerCancel() {
|
|
1329
|
+
state.answerDraft = null;
|
|
1330
|
+
controller.render();
|
|
1331
|
+
},
|
|
1332
|
+
async onAnswerConfirm(item) {
|
|
1333
|
+
const key = itemKey(item.proof);
|
|
1334
|
+
const draft = state.answerDraft;
|
|
1335
|
+
if (!draft || draft.key !== key) return;
|
|
1336
|
+
if (!request) {
|
|
1337
|
+
state.answerSave = { state: 'error', key, message: '記録先のAPIが未設定です。回答はまだ記録されていません。' };
|
|
1338
|
+
controller.render();
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
state.answerSave = { state: 'saving', key, message: '' };
|
|
1342
|
+
controller.render();
|
|
1343
|
+
let accepted = false;
|
|
1344
|
+
try {
|
|
1345
|
+
const response = await request(joinPath(basePath, '/answers'), {
|
|
1346
|
+
method: 'POST',
|
|
1347
|
+
headers: { 'Content-Type': 'application/json', 'X-Brainbase-Review-Token': token ?? '' },
|
|
1348
|
+
body: JSON.stringify({
|
|
1349
|
+
intent_id: item.proof.intent_id,
|
|
1350
|
+
decision_attempt_id: item.proof.decision_attempt_id,
|
|
1351
|
+
kind: draft.kind,
|
|
1352
|
+
selected_option_id: draft.kind === 'select' ? draft.optionId : null,
|
|
1353
|
+
confirmed: true,
|
|
1354
|
+
}),
|
|
1355
|
+
});
|
|
1356
|
+
if (response.status === 409) {
|
|
1357
|
+
// Another answer was recorded first; show that one instead of the one just picked.
|
|
1358
|
+
await controller.load();
|
|
1359
|
+
showJudgment(key);
|
|
1360
|
+
state.answerDraft = null;
|
|
1361
|
+
state.answerSave = { state: 'error', key, message: 'この問いには、すでに別の回答が記録されています。記録済みの回答を表示しています。' };
|
|
1362
|
+
controller.render();
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
if (!response.ok) throw new Error(await readErrorMessage(response));
|
|
1366
|
+
accepted = true;
|
|
1367
|
+
await controller.load();
|
|
1368
|
+
const answer = findItem(state, key)?.answer;
|
|
1369
|
+
if (!answer || answer.kind !== draft.kind || (draft.kind === 'select' && answer.optionId !== draft.optionId)) {
|
|
1370
|
+
throw new Error('読み戻しで回答を確認できません');
|
|
1371
|
+
}
|
|
1372
|
+
showJudgment(key);
|
|
1373
|
+
state.answerDraft = null;
|
|
1374
|
+
state.answerSave = { state: 'saved', key, message: '回答を記録しました(読み戻し済み)。' };
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
const reason = error instanceof Error ? error.message : 'request_failed';
|
|
1377
|
+
state.answerSave = {
|
|
1378
|
+
state: 'error',
|
|
1379
|
+
key,
|
|
1380
|
+
// Once the host accepted the answer, a failed read-back does not mean that nothing was recorded.
|
|
1381
|
+
message: accepted
|
|
1382
|
+
? `回答は送信しましたが、記録を確かめられませんでした: ${reason}。画面を読み込み直して、記録された回答を確かめてください。`
|
|
1383
|
+
: `記録できませんでした: ${reason}。回答はまだ記録されていません。`,
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
controller.render();
|
|
1387
|
+
},
|
|
1181
1388
|
async onConsult(value) {
|
|
1182
1389
|
if (!writeClipboard) {
|
|
1183
1390
|
state.consultMessage = `コピーできない環境です。次の文をCodexへ貼ってください。\n${value}`;
|
|
@@ -1193,8 +1400,25 @@ export function createValueProofReviewUI({
|
|
|
1193
1400
|
},
|
|
1194
1401
|
};
|
|
1195
1402
|
|
|
1403
|
+
/** Selects a judgment by decision_attempt_id; false when the loaded home does not have it. */
|
|
1404
|
+
const selectDecision = (decisionAttemptId) => {
|
|
1405
|
+
const item = findItemByDecision(state, decisionAttemptId);
|
|
1406
|
+
if (!item) return false;
|
|
1407
|
+
callbacks.onSelect(itemKey(item.proof));
|
|
1408
|
+
return true;
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1196
1411
|
const controller = {
|
|
1197
1412
|
get state() { return state; },
|
|
1413
|
+
/**
|
|
1414
|
+
* Opens one judgment, as a link from another screen does. Before the home
|
|
1415
|
+
* has loaded, the request waits for the load; an unknown id selects nothing.
|
|
1416
|
+
*/
|
|
1417
|
+
openDecision(decisionAttemptId) {
|
|
1418
|
+
if (state.phase === 'ready') return selectDecision(decisionAttemptId);
|
|
1419
|
+
state.pendingDecisionId = decisionAttemptId;
|
|
1420
|
+
return false;
|
|
1421
|
+
},
|
|
1198
1422
|
render() {
|
|
1199
1423
|
renderValueProofReview(root, state, callbacks, { document: doc, unavailableNotice, rail, page });
|
|
1200
1424
|
return controller;
|
|
@@ -1219,6 +1443,11 @@ export function createValueProofReviewUI({
|
|
|
1219
1443
|
state.error = error instanceof Error ? error.message : 'request_failed';
|
|
1220
1444
|
}
|
|
1221
1445
|
controller.render();
|
|
1446
|
+
if (state.phase === 'ready' && state.pendingDecisionId) {
|
|
1447
|
+
const pending = state.pendingDecisionId;
|
|
1448
|
+
state.pendingDecisionId = null;
|
|
1449
|
+
selectDecision(pending);
|
|
1450
|
+
}
|
|
1222
1451
|
return state.home;
|
|
1223
1452
|
},
|
|
1224
1453
|
callbacks,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Where a judgment stands in the world (ledger D6 placement).
|
|
3
|
+
*
|
|
4
|
+
* The judgment Host records the repository a turn worked in (`project_code`,
|
|
5
|
+
* the session's repository name). A judgment is placed in a business only by
|
|
6
|
+
* a relation the organization Graph already states:
|
|
7
|
+
* 1. the business registers that repository in `repository_roots`, or
|
|
8
|
+
* 2. the business code equals the repository name.
|
|
9
|
+
* Anything else stays in the plaza with the reason. The place is where the
|
|
10
|
+
* work happened, which is not always what the judgment was about.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const WORLD_PLACEMENT_CONTRACT_VERSION = 'brainbase.world-placement.v0';
|
|
14
|
+
|
|
15
|
+
export const UNPLACED_REASON_TEXT = Object.freeze({
|
|
16
|
+
workspace_unrecorded: '作業した場所の記録が無い',
|
|
17
|
+
workspace_unregistered: '作業した場所が、どの事業にも登録されていない',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/** @returns {{ business: object, basis: 'repository' | 'code' } | { business: null, reason: keyof UNPLACED_REASON_TEXT }} */
|
|
21
|
+
export function placeJudgment(workspace, businesses) {
|
|
22
|
+
if (typeof workspace !== 'string' || !workspace.trim()) return { business: null, reason: 'workspace_unrecorded' };
|
|
23
|
+
const name = workspace.trim();
|
|
24
|
+
const byRepository = businesses.find((business) => Array.isArray(business.repositories) && business.repositories.includes(name));
|
|
25
|
+
if (byRepository) return { business: byRepository, basis: 'repository' };
|
|
26
|
+
const byCode = businesses.find((business) => business.code === name);
|
|
27
|
+
if (byCode) return { business: byCode, basis: 'code' };
|
|
28
|
+
return { business: null, reason: 'workspace_unregistered' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Groups judgment places by business code; unplaced ones are listed with their reason. */
|
|
32
|
+
export function groupJudgmentPlaces(places, businesses) {
|
|
33
|
+
const byBusiness = new Map();
|
|
34
|
+
const unplaced = [];
|
|
35
|
+
for (const place of places) {
|
|
36
|
+
const result = placeJudgment(place.workspace, businesses);
|
|
37
|
+
if (result.business) {
|
|
38
|
+
if (!byBusiness.has(result.business.code)) byBusiness.set(result.business.code, []);
|
|
39
|
+
byBusiness.get(result.business.code).push({ ...place, basis: result.basis });
|
|
40
|
+
} else {
|
|
41
|
+
unplaced.push({ ...place, reason: result.reason });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { byBusiness, unplaced };
|
|
45
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
three 0.186.1
|
|
2
|
+
|
|
3
|
+
The MIT License
|
|
4
|
+
|
|
5
|
+
Copyright © 2010-2026 three.js authors
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in
|
|
15
|
+
all copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
THE SOFTWARE.
|