@unson/brainbase-mcp 0.41.8 → 0.41.10
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 +6 -0
- package/dist/canonical-task-service.js +18 -1
- package/dist/foundation-graph-http.js +5 -1
- package/dist/graph-foundation-reader.d.ts +27 -2
- package/dist/graph-foundation-reader.js +97 -1
- package/dist/meeting-minutes-http.js +19 -0
- package/dist/meeting-minutes.js +4 -0
- package/package.json +2 -2
- package/ui/foundation-overview.js +70 -14
- package/ui/meeting-minutes.css +151 -4
- package/ui/meeting-minutes.js +416 -94
- package/ui/workspace-kit.css +42 -0
- package/ui/workspace-kit.js +165 -0
- package/ui/world/world-canvas-notes.js +2 -1
- package/ui/world/world-canvas-ui.css +48 -3
- package/ui/world/world-canvas-ui.js +17 -4
- package/ui/world/world-district.js +34 -5
- package/ui/world/world-view.css +6 -0
- package/ui/world/world-view.js +109 -19
|
@@ -31,6 +31,8 @@ export interface CanonicalTaskRecord extends JsonRecord {
|
|
|
31
31
|
assignee_display_name?: string | null;
|
|
32
32
|
due_at?: string | null;
|
|
33
33
|
waiting_on?: string | null;
|
|
34
|
+
/** The Graph person a waiting task waits on; null when it waits on a condition or nobody in particular. */
|
|
35
|
+
waiting_on_person_id?: string | null;
|
|
34
36
|
review_at?: string | null;
|
|
35
37
|
completed_at?: string | null;
|
|
36
38
|
source_refs?: unknown[];
|
|
@@ -50,6 +52,7 @@ export interface CanonicalTaskCreateInput extends JsonRecord {
|
|
|
50
52
|
assignee_person_id?: string | null;
|
|
51
53
|
due_at?: string | null;
|
|
52
54
|
waiting_on?: string | null;
|
|
55
|
+
waiting_on_person_id?: string | null;
|
|
53
56
|
review_at?: string | null;
|
|
54
57
|
source_refs?: unknown[];
|
|
55
58
|
project_codes?: unknown;
|
|
@@ -63,6 +66,7 @@ export interface CanonicalTaskUpdateInput extends JsonRecord {
|
|
|
63
66
|
assignee_person_id?: string | null;
|
|
64
67
|
due_at?: string | null;
|
|
65
68
|
waiting_on?: string | null;
|
|
69
|
+
waiting_on_person_id?: string | null;
|
|
66
70
|
review_at?: string | null;
|
|
67
71
|
source_refs?: unknown[];
|
|
68
72
|
source_refs_append?: unknown[];
|
|
@@ -231,12 +235,14 @@ export declare class CanonicalTaskService {
|
|
|
231
235
|
}, context: CanonicalTaskContext): Promise<CanonicalTaskRecord>;
|
|
232
236
|
transitionTask(taskId: string, status: unknown, expectedVersion: number, context: CanonicalTaskContext, input?: {
|
|
233
237
|
waitingOn?: unknown;
|
|
238
|
+
waitingOnPersonId?: unknown;
|
|
234
239
|
reviewAt?: unknown;
|
|
235
240
|
}): Promise<CanonicalTaskRecord>;
|
|
236
241
|
transitionTask(taskId: string, input: {
|
|
237
242
|
to_status?: unknown;
|
|
238
243
|
expected_version?: number;
|
|
239
244
|
waiting_on?: unknown;
|
|
245
|
+
waiting_on_person_id?: unknown;
|
|
240
246
|
review_at?: unknown;
|
|
241
247
|
}, context: CanonicalTaskContext): Promise<CanonicalTaskRecord>;
|
|
242
248
|
deleteTask(taskId: string, expectedVersion: number, context: CanonicalTaskContext): Promise<{
|
|
@@ -34,6 +34,7 @@ const CREATE_FIELDS = new Set([
|
|
|
34
34
|
'assignee_person_id',
|
|
35
35
|
'due_at',
|
|
36
36
|
'waiting_on',
|
|
37
|
+
'waiting_on_person_id',
|
|
37
38
|
'review_at',
|
|
38
39
|
'source_refs',
|
|
39
40
|
'project_codes',
|
|
@@ -48,6 +49,7 @@ const MUTABLE_FIELDS = new Set([
|
|
|
48
49
|
'assignee_person_id',
|
|
49
50
|
'due_at',
|
|
50
51
|
'waiting_on',
|
|
52
|
+
'waiting_on_person_id',
|
|
51
53
|
'review_at',
|
|
52
54
|
'source_refs',
|
|
53
55
|
'source_refs_append',
|
|
@@ -78,6 +80,10 @@ function normalizeString(value, field, options = {}) {
|
|
|
78
80
|
}
|
|
79
81
|
return normalized;
|
|
80
82
|
}
|
|
83
|
+
/** A Graph person ID, as for the assignee; empty or null means no person. */
|
|
84
|
+
function normalizeWaitingOnPersonId(value) {
|
|
85
|
+
return normalizeString(value, 'waiting_on_person_id', { max: 200 });
|
|
86
|
+
}
|
|
81
87
|
/** A purpose label: a single line of at most 30 characters; empty or null clears it. */
|
|
82
88
|
const MAX_PURPOSE_LABEL_LENGTH = 30;
|
|
83
89
|
function normalizePurposeLabel(value) {
|
|
@@ -511,7 +517,7 @@ export class CanonicalTaskService {
|
|
|
511
517
|
const expectedVersion = legacyInput ? legacyInput.expected_version : expectedVersionOrContext;
|
|
512
518
|
const context = legacyInput ? expectedVersionOrContext : maybeContext;
|
|
513
519
|
const input = legacyInput
|
|
514
|
-
? { waitingOn: legacyInput.waiting_on, reviewAt: legacyInput.review_at }
|
|
520
|
+
? { waitingOn: legacyInput.waiting_on, waitingOnPersonId: legacyInput.waiting_on_person_id, reviewAt: legacyInput.review_at }
|
|
515
521
|
: maybeInput;
|
|
516
522
|
const normalizedContext = await this.normalizeContext(context);
|
|
517
523
|
const expected = this.requireExpectedVersion(expectedVersion);
|
|
@@ -523,10 +529,13 @@ export class CanonicalTaskService {
|
|
|
523
529
|
failValidation({ waiting_on: ['required_for_waiting'] });
|
|
524
530
|
}
|
|
525
531
|
patch.waiting_on = normalizeString(input.waitingOn, 'waiting_on', { required: true, max: 500 });
|
|
532
|
+
// The person is optional and always replaced, so waiting on a condition clears an earlier person.
|
|
533
|
+
patch.waiting_on_person_id = normalizeWaitingOnPersonId(input.waitingOnPersonId);
|
|
526
534
|
patch.review_at = normalizeIsoDate(input.reviewAt, 'review_at');
|
|
527
535
|
}
|
|
528
536
|
else {
|
|
529
537
|
patch.waiting_on = input.waitingOn === undefined ? null : normalizeString(input.waitingOn, 'waiting_on', { max: 500 });
|
|
538
|
+
patch.waiting_on_person_id = input.waitingOnPersonId === undefined ? null : normalizeWaitingOnPersonId(input.waitingOnPersonId);
|
|
530
539
|
patch.review_at = input.reviewAt === undefined ? null : normalizeIsoDate(input.reviewAt, 'review_at');
|
|
531
540
|
}
|
|
532
541
|
return this.versionedMutation({
|
|
@@ -801,6 +810,7 @@ export class CanonicalTaskService {
|
|
|
801
810
|
transition: {
|
|
802
811
|
to_status: input.transitionStatus,
|
|
803
812
|
...(input.patch.waiting_on !== undefined ? { waiting_on: input.patch.waiting_on } : {}),
|
|
813
|
+
...(input.patch.waiting_on_person_id !== undefined ? { waiting_on_person_id: input.patch.waiting_on_person_id } : {}),
|
|
804
814
|
...(input.patch.review_at !== undefined ? { review_at: input.patch.review_at } : {}),
|
|
805
815
|
},
|
|
806
816
|
};
|
|
@@ -818,6 +828,7 @@ export class CanonicalTaskService {
|
|
|
818
828
|
if (!isCanonicalTaskPriority(priority))
|
|
819
829
|
fail('validation_error', 'priority is invalid', 400, { field: 'priority' });
|
|
820
830
|
const purposeLabel = normalizePurposeLabel(input.purpose_label);
|
|
831
|
+
const waitingOnPersonId = normalizeWaitingOnPersonId(input.waiting_on_person_id);
|
|
821
832
|
const completionContract = normalizeCompletionContract(input.completion_contract);
|
|
822
833
|
return {
|
|
823
834
|
...(completionContract ? { completion_contract: completionContract } : {}),
|
|
@@ -832,6 +843,8 @@ export class CanonicalTaskService {
|
|
|
832
843
|
project_codes: normalizeProjectCodes(input.project_codes) ?? [],
|
|
833
844
|
// Only a label that is there is written, so a store without the column still accepts unlabelled work.
|
|
834
845
|
...(purposeLabel ? { purpose_label: purposeLabel } : {}),
|
|
846
|
+
// Likewise only a person that is there is written.
|
|
847
|
+
...(waitingOnPersonId ? { waiting_on_person_id: waitingOnPersonId } : {}),
|
|
835
848
|
};
|
|
836
849
|
}
|
|
837
850
|
normalizeUpdateInput(input) {
|
|
@@ -866,6 +879,8 @@ export class CanonicalTaskService {
|
|
|
866
879
|
patch.due_at = normalizeIsoDate(input.due_at, 'due_at');
|
|
867
880
|
if ('waiting_on' in input)
|
|
868
881
|
patch.waiting_on = normalizeString(input.waiting_on, 'waiting_on', { max: 500 });
|
|
882
|
+
if ('waiting_on_person_id' in input)
|
|
883
|
+
patch.waiting_on_person_id = normalizeWaitingOnPersonId(input.waiting_on_person_id);
|
|
869
884
|
if ('review_at' in input)
|
|
870
885
|
patch.review_at = normalizeIsoDate(input.review_at, 'review_at');
|
|
871
886
|
if ('source_refs' in input && hasSourceRefsAppend) {
|
|
@@ -1052,6 +1067,8 @@ export class CanonicalTaskService {
|
|
|
1052
1067
|
response.due_at = null;
|
|
1053
1068
|
if (response.waiting_on === undefined)
|
|
1054
1069
|
response.waiting_on = null;
|
|
1070
|
+
if (response.waiting_on_person_id === undefined)
|
|
1071
|
+
response.waiting_on_person_id = null;
|
|
1055
1072
|
if (response.review_at === undefined)
|
|
1056
1073
|
response.review_at = null;
|
|
1057
1074
|
if (response.completed_at === undefined)
|
|
@@ -253,13 +253,17 @@ function createFoundationCatalogRoute(readers, scopeId, context) {
|
|
|
253
253
|
const variables = await readers.store.list('variable', context);
|
|
254
254
|
const models = await readers.store.list('model', context);
|
|
255
255
|
const philosophies = await readers.listPhilosophies(context);
|
|
256
|
+
// Philosophies without recorded applicability are still read by the
|
|
257
|
+
// judgment frame catalog; report them separately rather than as absent.
|
|
258
|
+
const philosophiesWithoutApplicability = await readers.listUnscopedPhilosophies(context);
|
|
256
259
|
return jsonResponse(200, {
|
|
257
260
|
contractVersion: FOUNDATION_OVERVIEW_CONTRACT_VERSION,
|
|
258
261
|
scopeId,
|
|
259
262
|
objectives,
|
|
260
263
|
variables,
|
|
261
264
|
models,
|
|
262
|
-
philosophies
|
|
265
|
+
philosophies,
|
|
266
|
+
philosophies_without_applicability: philosophiesWithoutApplicability
|
|
263
267
|
});
|
|
264
268
|
}
|
|
265
269
|
};
|
|
@@ -25,7 +25,7 @@ export declare const GRAPH_FOUNDATION_LATEST_SQL = "\nSELECT\n history.entity_i
|
|
|
25
25
|
* project. The project filter is by canonical project code, which is the
|
|
26
26
|
* scope identifier selected by the organization BFF.
|
|
27
27
|
*/
|
|
28
|
-
export declare const GRAPH_FOUNDATION_LIST_SQL = "\nSELECT\n history.entity_id,\n history.entity_type,\n history.revision,\n history.payload,\n history.project_id,\n history.role_min,\n history.sensitivity,\n history.lifecycle_status,\n history.storage_digest,\n history.captured_at,\n (\n history.storage_digest = 'sha256:' ||\n encode(sha256(convert_to(history.payload::text, 'UTF8')), 'hex')\n ) AS storage_digest_valid,\n current_entity.id AS current_entity_id,\n current_entity.entity_type AS current_entity_type,\n current_entity.payload AS current_payload,\n current_entity.project_id AS current_project_id,\n current_entity.role_min AS current_role_min,\n current_entity.sensitivity AS current_sensitivity,\n current_entity.lifecycle_status AS current_lifecycle_status,\n projects.code AS current_project_code,\n true AS current_visible\nFROM public.graph_entities AS current_entity\nLEFT JOIN public.projects AS projects\n ON projects.id = current_entity.project_id\nLEFT JOIN LATERAL (\n SELECT history.*\n FROM public.graph_foundation_revisions AS history\n WHERE history.entity_id = current_entity.id\n AND history.entity_type = current_entity.entity_type\n ORDER BY history.revision::numeric DESC\n LIMIT 1\n) AS history ON true\nWHERE ($1::text IS NULL OR current_entity.entity_type = $1)\n AND ($2::text IS NULL OR projects.code = $2 OR projects.id IS NULL)\n AND current_entity.entity_type IN ('objective', 'variable', 'model', 'constraint')\nORDER BY current_entity.id, current_entity.entity_type\n";
|
|
28
|
+
export declare const GRAPH_FOUNDATION_LIST_SQL = "\nSELECT\n history.entity_id,\n history.entity_type,\n history.revision,\n history.payload,\n history.project_id,\n history.role_min,\n history.sensitivity,\n history.lifecycle_status,\n history.storage_digest,\n history.captured_at,\n (\n history.storage_digest = 'sha256:' ||\n encode(sha256(convert_to(history.payload::text, 'UTF8')), 'hex')\n ) AS storage_digest_valid,\n current_entity.id AS current_entity_id,\n current_entity.entity_type AS current_entity_type,\n current_entity.payload AS current_payload,\n current_entity.project_id AS current_project_id,\n current_entity.role_min AS current_role_min,\n current_entity.sensitivity AS current_sensitivity,\n current_entity.lifecycle_status AS current_lifecycle_status,\n projects.code AS current_project_code,\n true AS current_visible\nFROM public.graph_entities AS current_entity\nLEFT JOIN public.projects AS projects\n ON projects.id = current_entity.project_id\nLEFT JOIN LATERAL (\n SELECT history.*\n FROM public.graph_foundation_revisions AS history\n WHERE history.entity_id = current_entity.id\n AND history.entity_type = current_entity.entity_type\n ORDER BY history.revision::numeric DESC\n LIMIT 1\n) AS history ON true\nWHERE ($1::text IS NULL OR current_entity.entity_type = $1)\n AND ($2::text IS NULL OR projects.code = $2 OR projects.id IS NULL)\n AND current_entity.entity_type IN ('objective', 'variable', 'model', 'constraint')\n -- A list projects current records only. A retired row is not current, and\n -- reading it would make one replaced record fail the whole list.\n AND current_entity.lifecycle_status = 'active'\nORDER BY current_entity.id, current_entity.entity_type\n";
|
|
29
29
|
/**
|
|
30
30
|
* List the newest immutable revision for each philosophy attached to the
|
|
31
31
|
* selected project. The current Graph row is still the authorization
|
|
@@ -33,7 +33,29 @@ export declare const GRAPH_FOUNDATION_LIST_SQL = "\nSELECT\n history.entity_id,
|
|
|
33
33
|
* Unlike the Foundation list, this query requires a canonical project join so
|
|
34
34
|
* a missing project membership cannot be interpreted as a selected-scope hit.
|
|
35
35
|
*/
|
|
36
|
-
export declare const GRAPH_PHILOSOPHY_LIST_SQL = "\nSELECT\n history.entity_id,\n history.entity_type,\n history.revision,\n history.payload,\n history.project_id,\n history.role_min,\n history.sensitivity,\n history.lifecycle_status,\n history.storage_digest,\n history.captured_at,\n (\n history.storage_digest = 'sha256:' ||\n encode(sha256(convert_to(history.payload::text, 'UTF8')), 'hex')\n ) AS storage_digest_valid,\n current_entity.id AS current_entity_id,\n current_entity.entity_type AS current_entity_type,\n current_entity.payload AS current_payload,\n current_entity.project_id AS current_project_id,\n current_entity.role_min AS current_role_min,\n current_entity.sensitivity AS current_sensitivity,\n current_entity.lifecycle_status AS current_lifecycle_status,\n projects.code AS current_project_code,\n true AS current_visible\nFROM public.graph_entities AS current_entity\nJOIN public.projects AS projects\n ON projects.id = current_entity.project_id\nLEFT JOIN LATERAL (\n SELECT history.*\n FROM public.graph_foundation_revisions AS history\n WHERE history.entity_id = current_entity.id\n AND history.entity_type = current_entity.entity_type\n ORDER BY history.revision::numeric DESC\n LIMIT 1\n) AS history ON true\nWHERE current_entity.entity_type = 'philosophy'\n AND projects.code = $1\n AND current_entity.payload ? 'judgmentApplicability'\nORDER BY current_entity.id\n";
|
|
36
|
+
export declare const GRAPH_PHILOSOPHY_LIST_SQL = "\nSELECT\n history.entity_id,\n history.entity_type,\n history.revision,\n history.payload,\n history.project_id,\n history.role_min,\n history.sensitivity,\n history.lifecycle_status,\n history.storage_digest,\n history.captured_at,\n (\n history.storage_digest = 'sha256:' ||\n encode(sha256(convert_to(history.payload::text, 'UTF8')), 'hex')\n ) AS storage_digest_valid,\n current_entity.id AS current_entity_id,\n current_entity.entity_type AS current_entity_type,\n current_entity.payload AS current_payload,\n current_entity.project_id AS current_project_id,\n current_entity.role_min AS current_role_min,\n current_entity.sensitivity AS current_sensitivity,\n current_entity.lifecycle_status AS current_lifecycle_status,\n projects.code AS current_project_code,\n true AS current_visible\nFROM public.graph_entities AS current_entity\nJOIN public.projects AS projects\n ON projects.id = current_entity.project_id\nLEFT JOIN LATERAL (\n SELECT history.*\n FROM public.graph_foundation_revisions AS history\n WHERE history.entity_id = current_entity.id\n AND history.entity_type = current_entity.entity_type\n ORDER BY history.revision::numeric DESC\n LIMIT 1\n) AS history ON true\nWHERE current_entity.entity_type = 'philosophy'\n AND projects.code = $1\n AND current_entity.lifecycle_status = 'active'\n AND current_entity.payload ? 'judgmentApplicability'\nORDER BY current_entity.id\n";
|
|
37
|
+
/**
|
|
38
|
+
* Current philosophies of the selected project that predate recorded
|
|
39
|
+
* applicability. They are excluded from the verified list above, but the
|
|
40
|
+
* judgment frame catalog still reads them, so the overview must show them as
|
|
41
|
+
* "applicability unrecorded" instead of reporting that none are registered.
|
|
42
|
+
*/
|
|
43
|
+
export declare const GRAPH_UNSCOPED_PHILOSOPHY_LIST_SQL = "\nSELECT\n current_entity.id AS current_entity_id,\n current_entity.entity_type AS current_entity_type,\n current_entity.payload AS current_payload,\n current_entity.project_id AS current_project_id,\n current_entity.lifecycle_status AS current_lifecycle_status,\n current_entity.version AS current_version,\n projects.code AS current_project_code\nFROM public.graph_entities AS current_entity\nJOIN public.projects AS projects\n ON projects.id = current_entity.project_id\nWHERE current_entity.entity_type = 'philosophy'\n AND projects.code = $1\n AND current_entity.lifecycle_status = 'active'\n AND NOT (current_entity.payload ? 'judgmentApplicability')\nORDER BY current_entity.id\n";
|
|
44
|
+
/** A current philosophy whose judgment applicability has never been recorded. */
|
|
45
|
+
export interface UnscopedPhilosophyRecord {
|
|
46
|
+
readonly kind: 'philosophy';
|
|
47
|
+
readonly id: string;
|
|
48
|
+
/** Current Graph version; there is no verified foundation revision. */
|
|
49
|
+
readonly version: number;
|
|
50
|
+
readonly applicability: null;
|
|
51
|
+
readonly applicability_state: 'unrecorded';
|
|
52
|
+
readonly payload: {
|
|
53
|
+
readonly title?: string;
|
|
54
|
+
readonly statement: string;
|
|
55
|
+
readonly summary?: string;
|
|
56
|
+
readonly status?: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
37
59
|
/** Flat row shape returned by GRAPH_FOUNDATION_READ_SQL. */
|
|
38
60
|
export interface GraphFoundationHistoryRow {
|
|
39
61
|
readonly entity_id?: unknown;
|
|
@@ -55,6 +77,7 @@ export interface GraphFoundationHistoryRow {
|
|
|
55
77
|
readonly current_role_min?: unknown;
|
|
56
78
|
readonly current_sensitivity?: unknown;
|
|
57
79
|
readonly current_lifecycle_status?: unknown;
|
|
80
|
+
readonly current_version?: unknown;
|
|
58
81
|
readonly current_visible?: unknown;
|
|
59
82
|
}
|
|
60
83
|
export interface GraphFoundationQueryResult {
|
|
@@ -91,6 +114,8 @@ export interface GraphFoundationReaders {
|
|
|
91
114
|
readonly philosophyReader: PhilosophyRevisionReader;
|
|
92
115
|
/** List latest, current-visible philosophy revisions for the selected project. */
|
|
93
116
|
readonly listPhilosophies: (context: FoundationStoreContext) => Promise<PhilosophyRevisionRecord[]>;
|
|
117
|
+
/** List current philosophies of the selected project without recorded applicability. */
|
|
118
|
+
readonly listUnscopedPhilosophies: (context: FoundationStoreContext) => Promise<UnscopedPhilosophyRecord[]>;
|
|
94
119
|
}
|
|
95
120
|
/**
|
|
96
121
|
* Create both public read ports over one trusted, transaction-bound context.
|
|
@@ -141,6 +141,9 @@ LEFT JOIN LATERAL (
|
|
|
141
141
|
WHERE ($1::text IS NULL OR current_entity.entity_type = $1)
|
|
142
142
|
AND ($2::text IS NULL OR projects.code = $2 OR projects.id IS NULL)
|
|
143
143
|
AND current_entity.entity_type IN ('objective', 'variable', 'model', 'constraint')
|
|
144
|
+
-- A list projects current records only. A retired row is not current, and
|
|
145
|
+
-- reading it would make one replaced record fail the whole list.
|
|
146
|
+
AND current_entity.lifecycle_status = 'active'
|
|
144
147
|
ORDER BY current_entity.id, current_entity.entity_type
|
|
145
148
|
`;
|
|
146
149
|
/**
|
|
@@ -188,9 +191,34 @@ LEFT JOIN LATERAL (
|
|
|
188
191
|
) AS history ON true
|
|
189
192
|
WHERE current_entity.entity_type = 'philosophy'
|
|
190
193
|
AND projects.code = $1
|
|
194
|
+
AND current_entity.lifecycle_status = 'active'
|
|
191
195
|
AND current_entity.payload ? 'judgmentApplicability'
|
|
192
196
|
ORDER BY current_entity.id
|
|
193
197
|
`;
|
|
198
|
+
/**
|
|
199
|
+
* Current philosophies of the selected project that predate recorded
|
|
200
|
+
* applicability. They are excluded from the verified list above, but the
|
|
201
|
+
* judgment frame catalog still reads them, so the overview must show them as
|
|
202
|
+
* "applicability unrecorded" instead of reporting that none are registered.
|
|
203
|
+
*/
|
|
204
|
+
export const GRAPH_UNSCOPED_PHILOSOPHY_LIST_SQL = `
|
|
205
|
+
SELECT
|
|
206
|
+
current_entity.id AS current_entity_id,
|
|
207
|
+
current_entity.entity_type AS current_entity_type,
|
|
208
|
+
current_entity.payload AS current_payload,
|
|
209
|
+
current_entity.project_id AS current_project_id,
|
|
210
|
+
current_entity.lifecycle_status AS current_lifecycle_status,
|
|
211
|
+
current_entity.version AS current_version,
|
|
212
|
+
projects.code AS current_project_code
|
|
213
|
+
FROM public.graph_entities AS current_entity
|
|
214
|
+
JOIN public.projects AS projects
|
|
215
|
+
ON projects.id = current_entity.project_id
|
|
216
|
+
WHERE current_entity.entity_type = 'philosophy'
|
|
217
|
+
AND projects.code = $1
|
|
218
|
+
AND current_entity.lifecycle_status = 'active'
|
|
219
|
+
AND NOT (current_entity.payload ? 'judgmentApplicability')
|
|
220
|
+
ORDER BY current_entity.id
|
|
221
|
+
`;
|
|
194
222
|
const FOUNDATION_TYPES = ['objective', 'variable', 'model', 'constraint'];
|
|
195
223
|
const REVISION_PATTERN = /^[1-9]\d*$/u;
|
|
196
224
|
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
@@ -284,7 +312,75 @@ export function createGraphFoundationReaders(options) {
|
|
|
284
312
|
return createPhilosophyRevisionRecord(validated, id, revision, context.principal);
|
|
285
313
|
});
|
|
286
314
|
}
|
|
287
|
-
|
|
315
|
+
async function listUnscopedPhilosophies(suppliedContext) {
|
|
316
|
+
assertTrustedPrincipal(context, suppliedContext);
|
|
317
|
+
if (selectedProjectCode === undefined) {
|
|
318
|
+
throw scopeViolation('A selected project is required to list philosophies');
|
|
319
|
+
}
|
|
320
|
+
let result;
|
|
321
|
+
try {
|
|
322
|
+
result = await query(GRAPH_UNSCOPED_PHILOSOPHY_LIST_SQL, [selectedProjectCode]);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
throw corrupt('Graph unscoped philosophy list query failed');
|
|
326
|
+
}
|
|
327
|
+
const rows = Array.isArray(result) ? result
|
|
328
|
+
: 'rows' in result && Array.isArray(result.rows) ? result.rows
|
|
329
|
+
: null;
|
|
330
|
+
if (!rows)
|
|
331
|
+
throw corrupt('Graph unscoped philosophy list query returned an invalid result');
|
|
332
|
+
const seen = new Set();
|
|
333
|
+
const records = [];
|
|
334
|
+
for (const row of rows) {
|
|
335
|
+
const id = requireString(row.current_entity_id, 'Current philosophy id');
|
|
336
|
+
if (row.current_entity_type !== 'philosophy')
|
|
337
|
+
throw corrupt('Graph philosophy list returned another entity type');
|
|
338
|
+
if (row.current_project_code !== selectedProjectCode) {
|
|
339
|
+
throw scopeViolation('The current philosophy project is outside the selected project scope');
|
|
340
|
+
}
|
|
341
|
+
if (seen.has(id))
|
|
342
|
+
throw corrupt('Philosophy list returned duplicate entities');
|
|
343
|
+
seen.add(id);
|
|
344
|
+
const version = Number(row.current_version);
|
|
345
|
+
if (!Number.isSafeInteger(version) || version < 1)
|
|
346
|
+
throw corrupt('Current philosophy version is invalid');
|
|
347
|
+
const payload = requireObject(row.current_payload, 'Current philosophy payload');
|
|
348
|
+
if (Object.hasOwn(payload, 'judgmentApplicability')) {
|
|
349
|
+
throw corrupt('Unscoped philosophy list returned a philosophy with applicability');
|
|
350
|
+
}
|
|
351
|
+
// Private philosophies always carry applicability; an owner-only ACL here
|
|
352
|
+
// is still enforced rather than silently shown.
|
|
353
|
+
assertPrivatePhilosophyRead(payload, context.principal);
|
|
354
|
+
// Mirror the judgment frame catalog: superseded or text-less records are
|
|
355
|
+
// not used for judgment, so they are not shown as usable either.
|
|
356
|
+
const status = optionalText(payload.status);
|
|
357
|
+
if (status && /^(superseded|retired|rejected)/u.test(status))
|
|
358
|
+
continue;
|
|
359
|
+
const statement = optionalText(payload.statement);
|
|
360
|
+
if (!statement)
|
|
361
|
+
continue;
|
|
362
|
+
const title = optionalText(payload.display_name) ?? optionalText(payload.title);
|
|
363
|
+
const summary = optionalText(payload.summary);
|
|
364
|
+
records.push({
|
|
365
|
+
kind: 'philosophy',
|
|
366
|
+
id,
|
|
367
|
+
version,
|
|
368
|
+
applicability: null,
|
|
369
|
+
applicability_state: 'unrecorded',
|
|
370
|
+
payload: {
|
|
371
|
+
...(title ? { title } : {}),
|
|
372
|
+
statement,
|
|
373
|
+
...(summary ? { summary } : {}),
|
|
374
|
+
...(status ? { status } : {})
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return records;
|
|
379
|
+
}
|
|
380
|
+
return { store, philosophyReader, listPhilosophies, listUnscopedPhilosophies };
|
|
381
|
+
}
|
|
382
|
+
function optionalText(value) {
|
|
383
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
288
384
|
}
|
|
289
385
|
function createPhilosophyRevisionRecord(validated, id, revision, principal) {
|
|
290
386
|
return {
|
|
@@ -44,6 +44,9 @@ function routeFor(pathname, basePath) {
|
|
|
44
44
|
if (segments.length === 4 && noun === 'minutes' && minutesId && action === 'confirm') {
|
|
45
45
|
return { kind: 'confirm', meetingId, minutesId };
|
|
46
46
|
}
|
|
47
|
+
if (segments.length === 4 && noun === 'minutes' && minutesId && action === 'withdraw') {
|
|
48
|
+
return { kind: 'withdraw', meetingId, minutesId };
|
|
49
|
+
}
|
|
47
50
|
if (segments.length === 5 && noun === 'minutes' && minutesId && action === 'versions' && versionId) {
|
|
48
51
|
return { kind: 'version', meetingId, minutesId, versionId };
|
|
49
52
|
}
|
|
@@ -157,6 +160,13 @@ function confirmInput(body, minutesId, key, fingerprint) {
|
|
|
157
160
|
...idempotencyFields(key, fingerprint)
|
|
158
161
|
};
|
|
159
162
|
}
|
|
163
|
+
function withdrawInput(body, minutesId, key, fingerprint) {
|
|
164
|
+
return {
|
|
165
|
+
minutes_id: minutesId,
|
|
166
|
+
expected_revision: body.expected_revision,
|
|
167
|
+
...idempotencyFields(key, fingerprint)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
160
170
|
export function createMeetingMinutesHttpHandler(options) {
|
|
161
171
|
if (!options || !options.store || typeof options.resolveContext !== 'function') {
|
|
162
172
|
throw new TypeError('store and resolveContext are required');
|
|
@@ -202,6 +212,15 @@ export function createMeetingMinutesHttpHandler(options) {
|
|
|
202
212
|
}
|
|
203
213
|
return { statusCode: 200, body: await options.store.confirm_version(confirmInput(body, document.minutes_id, key, fingerprint), context) };
|
|
204
214
|
}
|
|
215
|
+
case 'withdraw': {
|
|
216
|
+
if (body === undefined)
|
|
217
|
+
throw new MeetingMinutesError('invalid_input', 'Request body is required');
|
|
218
|
+
const detail = await options.store.get_meeting(route.meetingId ?? '', context);
|
|
219
|
+
const document = detail.minutes.find((candidate) => candidate.minutes_id === route.minutesId);
|
|
220
|
+
if (!document)
|
|
221
|
+
throw new MeetingMinutesError('not_found', `Minutes document ${route.minutesId} was not found`);
|
|
222
|
+
return { statusCode: 200, body: await options.store.withdraw_minutes(withdrawInput(body, document.minutes_id, key, fingerprint), context) };
|
|
223
|
+
}
|
|
205
224
|
case 'version':
|
|
206
225
|
return {
|
|
207
226
|
statusCode: 200,
|
package/dist/meeting-minutes.js
CHANGED
|
@@ -1132,6 +1132,10 @@ export function createMeetingMinutesStore(options) {
|
|
|
1132
1132
|
const meeting = findMeeting(snapshot, document.meeting_id);
|
|
1133
1133
|
await authorized(authorizer, 'write', meeting, context);
|
|
1134
1134
|
await authorized(authorizer, 'write', document, context);
|
|
1135
|
+
// Only the person who made the document takes it back (story-meeting-minutes-edit-and-withdraw-ui-v1).
|
|
1136
|
+
if (document.created_actor_id !== principal) {
|
|
1137
|
+
throw new MeetingMinutesError('authorization_denied', 'Only the person who made this minutes document can withdraw it');
|
|
1138
|
+
}
|
|
1135
1139
|
// Withdrawing twice keeps the first record; a retry is not a second withdrawal.
|
|
1136
1140
|
if (document.withdrawal && !idempotency.token)
|
|
1137
1141
|
return detailForCaller(snapshot, meeting, context);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.10",
|
|
4
4
|
"description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -483,5 +483,5 @@
|
|
|
483
483
|
"engines": {
|
|
484
484
|
"node": ">=20"
|
|
485
485
|
},
|
|
486
|
-
"gitHead": "
|
|
486
|
+
"gitHead": "9ea5345d8da4b0cf825daf73610474dd8903a8f5"
|
|
487
487
|
}
|
|
@@ -148,6 +148,22 @@ function validatePhilosophyRecords(value) {
|
|
|
148
148
|
return value;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
function validateUnscopedPhilosophyRecords(value) {
|
|
152
|
+
if (!Array.isArray(value)) throw error('invalid_schema', '適用範囲が未記録の哲学の形式を確認できません。');
|
|
153
|
+
for (const [index, item] of value.entries()) {
|
|
154
|
+
if (!isObject(item)
|
|
155
|
+
|| item.kind !== 'philosophy'
|
|
156
|
+
|| !nonEmpty(item.id)
|
|
157
|
+
|| !Number.isSafeInteger(item.version) || item.version < 1
|
|
158
|
+
|| item.applicability_state !== 'unrecorded'
|
|
159
|
+
|| !isObject(item.payload)
|
|
160
|
+
|| !nonEmpty(item.payload.statement)) {
|
|
161
|
+
throw error('invalid_schema', `適用範囲が未記録の哲学の${index + 1}件目の形式が不正です。`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
|
|
151
167
|
/** Validate the host contract without converting an unreadable response into an empty catalog. */
|
|
152
168
|
export function normalizeFoundationCatalog(value, expectedScopeId) {
|
|
153
169
|
if (!isObject(value) || value.contractVersion !== FOUNDATION_OVERVIEW_CONTRACT_VERSION) {
|
|
@@ -163,7 +179,10 @@ export function normalizeFoundationCatalog(value, expectedScopeId) {
|
|
|
163
179
|
const knownReadableStatuses = new Set(['complete', 'ready', 'resolved', 'empty']);
|
|
164
180
|
const unreadableStatus = (readStatus && !knownReadableStatuses.has(readStatus))
|
|
165
181
|
|| (catalogStatus && !knownReadableStatuses.has(catalogStatus));
|
|
166
|
-
|
|
182
|
+
// Older hosts omit this field; then it stays unknown instead of becoming [].
|
|
183
|
+
const unscopedPhilosophies = value.philosophies_without_applicability === undefined
|
|
184
|
+
? undefined : validateUnscopedPhilosophyRecords(value.philosophies_without_applicability);
|
|
185
|
+
const collections = [value.objectives, value.variables, value.models, value.philosophies, unscopedPhilosophies];
|
|
167
186
|
const hasRecords = collections.some((records) => Array.isArray(records) && records.length > 0);
|
|
168
187
|
const absenceConfirmed = value.absence_confirmed === true;
|
|
169
188
|
if (unreadableStatus || (!hasRecords && ('absence_confirmed' in value || readStatus || catalogStatus) && !absenceConfirmed)) {
|
|
@@ -180,6 +199,7 @@ export function normalizeFoundationCatalog(value, expectedScopeId) {
|
|
|
180
199
|
variables: value.variables,
|
|
181
200
|
models: value.models,
|
|
182
201
|
philosophies: value.philosophies,
|
|
202
|
+
...(unscopedPhilosophies ? { philosophies_without_applicability: unscopedPhilosophies } : {}),
|
|
183
203
|
...(readStatus ? { read_status: readStatus } : {}),
|
|
184
204
|
...(catalogStatus ? { status: catalogStatus } : {}),
|
|
185
205
|
...(typeof value.absence_confirmed === 'boolean' ? { absence_confirmed: value.absence_confirmed } : {}),
|
|
@@ -589,21 +609,53 @@ function philosophyCard(doc, record) {
|
|
|
589
609
|
return card;
|
|
590
610
|
}
|
|
591
611
|
|
|
612
|
+
function unscopedPhilosophyCard(doc, record, recordContexts) {
|
|
613
|
+
const payload = record.payload;
|
|
614
|
+
const card = makeElement(doc, 'article', { className: 'bb-fov-philosophy-card is-unscoped' });
|
|
615
|
+
card.append(makeElement(doc, 'h3', { text: formatAny(payload.title) ?? UNNAMED_RECORD }));
|
|
616
|
+
card.append(makeElement(doc, 'p', { className: 'bb-fov-philosophy-statement', text: payload.statement }));
|
|
617
|
+
if (formatAny(payload.summary) && payload.summary !== payload.statement) card.append(makeElement(doc, 'p', { text: payload.summary }));
|
|
618
|
+
// The section notice explains what an unrecorded applicability means.
|
|
619
|
+
card.append(workspaceDefinition(doc, [['適用範囲', '未記録']]));
|
|
620
|
+
const refs = makeElement(doc, 'details', { className: 'bb-fov-original' });
|
|
621
|
+
refs.append(makeElement(doc, 'summary', { text: '記録の詳細' }), workspaceDefinition(doc, [['出典', recordContexts?.get(record)?.label ?? '未記録'], ['状態', payload.status], ['Graphの版', record.version], ['記録ID', record.id]]));
|
|
622
|
+
card.append(refs);
|
|
623
|
+
return card;
|
|
624
|
+
}
|
|
625
|
+
|
|
592
626
|
function renderPhilosophies(doc, catalog, recordContexts) {
|
|
593
627
|
const section = makeElement(doc, 'section', { className: 'bb-fov-section', attrs: { 'aria-label': '哲学' } });
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
628
|
+
const unscoped = catalog.philosophies_without_applicability;
|
|
629
|
+
|
|
630
|
+
if (catalog.philosophies.length === 0 && !unscoped?.length) {
|
|
631
|
+
section.append(workspaceNotice(doc, {
|
|
632
|
+
label: '哲学',
|
|
633
|
+
text: unscoped
|
|
634
|
+
? 'この対象範囲には登録された哲学がありません。'
|
|
635
|
+
// Without the unscoped list, only the verified list is known to be empty.
|
|
636
|
+
: '適用範囲が記録された哲学はありません。適用範囲が未記録の哲学があるかは確認できていません。',
|
|
637
|
+
}));
|
|
597
638
|
return section;
|
|
598
639
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
640
|
+
if (catalog.philosophies.length) {
|
|
641
|
+
const grid = makeElement(doc, 'div', { className: 'bb-fov-philosophy-grid' });
|
|
642
|
+
for (const record of catalog.philosophies) {
|
|
643
|
+
const card = philosophyCard(doc, record);
|
|
644
|
+
const refs = makeElement(doc, 'details', { className: 'bb-fov-original' });
|
|
645
|
+
refs.append(makeElement(doc, 'summary', { text: '記録の詳細' }), workspaceDefinition(doc, [['出典', recordContexts?.get(record)?.label ?? '未記録'], ['状態', record.payload.status], ['版', record.revision], ['記録ID', record.id], ['digest', record.digest]]));
|
|
646
|
+
card.append(refs); grid.append(card);
|
|
647
|
+
}
|
|
648
|
+
section.append(grid);
|
|
649
|
+
}
|
|
650
|
+
if (unscoped?.length) {
|
|
651
|
+
section.append(workspaceNotice(doc, {
|
|
652
|
+
label: '適用範囲が未記録',
|
|
653
|
+
text: `次の${unscoped.length}件は、判断への適用範囲が記録されていない哲学です。判断では参照されますが、どの範囲に当てはまるかは確かめられていません。`,
|
|
654
|
+
}));
|
|
655
|
+
const grid = makeElement(doc, 'div', { className: 'bb-fov-philosophy-grid' });
|
|
656
|
+
for (const record of unscoped) grid.append(unscopedPhilosophyCard(doc, record, recordContexts));
|
|
657
|
+
section.append(grid);
|
|
605
658
|
}
|
|
606
|
-
section.append(grid);
|
|
607
659
|
return section;
|
|
608
660
|
}
|
|
609
661
|
|
|
@@ -829,10 +881,11 @@ export function renderFoundationCollectionOverview(root, sources, {
|
|
|
829
881
|
} = {}) {
|
|
830
882
|
const doc = getDocument(explicitDocument);
|
|
831
883
|
root.replaceChildren();
|
|
832
|
-
const catalog = { objectives: [], variables: [], models: [], philosophies: [] };
|
|
884
|
+
const catalog = { objectives: [], variables: [], models: [], philosophies: [], philosophies_without_applicability: [] };
|
|
833
885
|
const contexts = new WeakMap();
|
|
834
886
|
const seen = new Map();
|
|
835
887
|
let ready = 0;
|
|
888
|
+
let unscopedUnknown = false;
|
|
836
889
|
for (const source of sources) {
|
|
837
890
|
if (source.state.status !== 'ready') {
|
|
838
891
|
if (source.state.status === 'loading') {
|
|
@@ -846,8 +899,9 @@ export function renderFoundationCollectionOverview(root, sources, {
|
|
|
846
899
|
const value = normalizeFoundationCatalog(source.state.catalog, source.scopeId);
|
|
847
900
|
ready++;
|
|
848
901
|
const variableMap = new Map(value.variables.map((record) => [foundationKey(recordDefinition(record)), record]));
|
|
849
|
-
|
|
850
|
-
|
|
902
|
+
if (!value.philosophies_without_applicability) unscopedUnknown = true;
|
|
903
|
+
for (const kind of ['objectives', 'models', 'philosophies', 'philosophies_without_applicability']) {
|
|
904
|
+
for (const original of value[kind] ?? []) {
|
|
851
905
|
const definition = original.definition ?? original;
|
|
852
906
|
const scope = original.scope ?? original.currentScope ?? definition.scope;
|
|
853
907
|
// Scope-less/project records remain tied to their authenticated catalog.
|
|
@@ -868,6 +922,8 @@ export function renderFoundationCollectionOverview(root, sources, {
|
|
|
868
922
|
}
|
|
869
923
|
}
|
|
870
924
|
}
|
|
925
|
+
// A host that did not report unscoped philosophies leaves the absence unknown.
|
|
926
|
+
if (unscopedUnknown && catalog.philosophies_without_applicability.length === 0) delete catalog.philosophies_without_applicability;
|
|
871
927
|
if (ready) {
|
|
872
928
|
const mount = makeElement(doc, 'div');
|
|
873
929
|
renderFoundationOverview(mount, { status: 'ready', catalog }, {
|