-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathschemas.ts
More file actions
1729 lines (1651 loc) Β· 60.1 KB
/
Copy pathschemas.ts
File metadata and controls
1729 lines (1651 loc) Β· 60.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Schema definitions for fbrain's record types.
//
// Pattern status: fbrain's per-kind, purpose-split schemas are
// grandfathered historical catalog identities. Do not copy this as the
// default pattern for new apps; decision-2026-07-16-schema-identity-system-treatment
// says new apps use starter templates plus an explicit `kind` field for
// app-level meaning.
//
// fbrain registers one schema per record type, plus internal support schemas:
//
// - **Design**, **Task** β Phase 1, unchanged.
// - **Concept**, **Preference**, **Reference**, **Agent**, **Project**,
// **Spike** β Phase 6 kinds. As of Phase E (the dual-signal
// canonicalization cutover) each gets its own dedicated schema with
// a distinct `descriptive_name` + `purpose_statement`. The schema
// service's dual-signal gate uses the purpose-statement embedding to
// veto structural collapse, so all six can share the same 7-field
// shape without colliding onto a single canonical hash.
// - **Sop** β a later addition built on the same 7-field Phase 6 shape
// (same dedicated-schema + distinct-purpose-statement treatment), for
// storing standard operating procedures agents follow on recurring tasks.
//
// Why dedicated schemas now? Pre-Phase-E we used one shared
// `FbrainKindNote` schema + a `kind` discriminator + `v1_marker_a/b`
// structural-distinctness markers to defeat fold's structural
// canonicalization. With dual-signal canonicalization default-on,
// the structural collision is solved at the schema-service layer
// (distinct purpose statements veto the merge), so the workaround
// is unnecessary. After the consolidation migration (PR #63) moved
// every pre-Phase-E `FbrainKindNote` row into its per-kind canonical,
// the legacy schema is no longer registered or read from.
//
// `POST /v1/schemas` accepts these bodies; the response's `schema.name`
// IS THE CANONICAL HASH that every subsequent mutation/query MUST pin
// to. The descriptive_name is for human-facing display only.
// The app id that owns every fbrain schema. Under app_identity v3.1,
// `owner_app_id` is part of the schema's identity: the schema_service resolver
// normalizes `name: "Concept"` + `owner_app_id: "fbrain"` to the canonical
// name `fbrain/Concept` (design doc, "owner_app_id participates in
// identity_hash"). So `fbrain/Concept` and `kanban/Concept` are distinct
// identities even with identical fields. The publish path (`folddb-dev app
// publish` / `schema publish --app fbrain`, run by the developer in the
// migration runbook) authorizes this claim with a dev cert; fbrain's own
// schema definitions just declare ownership so re-registration is idempotent
// and the node resolves short names to the fbrain/* namespace at boot.
export const OWNER_APP_ID = "fbrain";
export type FieldType = "String" | "Any" | { Array: "String" };
export type SchemaDefinition = {
name: string;
// App-identity ownership. The schema_service folds this into the identity
// hash and stores the schema under the canonical name `{owner_app_id}/{name}`
// (= `fbrain/<Name>`). Set on every fbrain schema; optional in the TS type
// so externally-loaded schema definitions (e.g. legacy fixtures, or future
// non-fbrain catalogues) can omit it without a TS error.
owner_app_id?: string;
descriptive_name: string;
// Phase A of dual-signal canonicalization (PR #303): the schema service
// consults this alongside the structural signal at registration time.
// Defaults to `descriptive_name` server-side when omitted; the Phase 6
// schemas set it explicitly to distinguish themselves from each other
// (they all share the same field shape).
purpose_statement?: string;
// Every PRODUCT record schema is "Hash" (one row per slug). "HashRange"
// exists for the index plane β see `recordListEntrySchema`, where the row is
// addressed (hash_field, range_field) so a put patches one row instead of
// rewriting a whole-type rollup atom.
schema_type: "Hash" | "HashRange";
key: { hash_field: string; range_field?: string };
fields: string[];
field_types: Record<string, FieldType>;
field_descriptions: Record<string, string>;
field_classifications?: Record<string, string[]>;
field_data_classifications: Record<
string,
{ sensitivity_level: number; data_domain: string }
>;
};
export type AddSchemaRequest = {
schema: SchemaDefinition;
mutation_mappers: Record<string, string>;
};
export const DESIGN_STATUSES = [
"draft",
"reviewed",
"approved",
"implemented",
"archived",
] as const;
export const TASK_STATUSES = [
"open",
"in_progress",
"blocked",
"done",
"cancelled",
] as const;
export const CONCEPT_STATUSES = ["active", "parked", "archived"] as const;
export const PREFERENCE_STATUSES = ["active", "parked", "superseded"] as const;
export const REFERENCE_STATUSES = ["active", "parked", "broken", "archived"] as const;
export const AGENT_STATUSES = ["active", "archived"] as const;
export const PROJECT_STATUSES = [
"planning",
"in_progress",
"done",
"archived",
] as const;
export const SPIKE_STATUSES = ["active", "concluded"] as const;
export const SOP_STATUSES = ["active", "parked", "superseded", "archived"] as const;
// A `decision` is one call a human made. Status is the OUTCOME, not a
// workflow state: `go` (approved/proceed), `hold` (deferred/parked),
// `done` (decided AND the resulting work has landed), `moot` (the premise
// went away so no action is needed), `superseded` (a later decision
// replaced it). `proposed` is the pre-decision draft state. Replaces the
// single monolithic `decisions-log` reference record β one record per
// decision so appending is a tiny write, not a 19 KB rewrite.
export const DECISION_STATUSES = [
"proposed",
"go",
"hold",
"done",
"moot",
"superseded",
] as const;
// A `papercut` is one observed defect in our own tooling, with its evidence.
// Status is the REPAIR lifecycle, and the split that matters most is
// `fixed` vs `verified`: "merged" is a fact about a repository and is NOT
// evidence the defect is gone. Runs that conflated the two produced the
// measured failure this type exists to end β on 2026-08-04, 40 of 107
// prose-ledger records read OPEN at the top and closed at the bottom,
// because `brain append` cannot rewrite the `Status:` line it follows.
//
// open filed, not repaired
// partial some of it repaired, some still open (say which in the body)
// fixed a change merged that should resolve it β NOT yet re-measured
// verified a live check confirmed it gone (terminal)
// wontfix deliberately not repairing (terminal)
// duplicate superseded by another papercut β see `duplicate_of` (terminal)
export const PAPERCUT_STATUSES = [
"open",
"partial",
"fixed",
"verified",
"wontfix",
"duplicate",
] as const;
// Severity ladder. Kept a plain String column (not an enum type) to match
// every other field on the record; `brain papercut file` validates it.
export const PAPERCUT_SEVERITIES = ["p0", "p1", "p2", "p3"] as const;
// What KIND of thing the record is, which the prose ledger could not say β
// and the reason a fully specified fix sat unread for three days:
// nothing distinguished a finished proposal from a raw complaint.
//
// complaint an observation; nobody has worked out the repair yet
// specified-fix the repair is worked out and written down β ready to pick up
// reconfirmed a previously-closed papercut re-measured as still live
export const PAPERCUT_KINDS = [
"complaint",
"specified-fix",
"reconfirmed",
] as const;
const GENERAL = { sensitivity_level: 0, data_domain: "general" };
// The seven-field shape shared by Design + all six Phase 6 kinds. Building
// each per-kind schema from the same template ensures their structural
// signatures match exactly; the per-kind `descriptive_name` + `purpose_statement`
// is what keeps the dual-signal gate from merging them.
const PHASE_6_FIELDS = [
"slug",
"title",
"body",
"status",
"tags",
"created_at",
"updated_at",
] as const;
const PHASE_6_FIELD_TYPES: Record<string, FieldType> = {
slug: "String",
title: "String",
body: "String",
status: "String",
tags: { Array: "String" },
created_at: "String",
updated_at: "String",
};
const PHASE_6_FIELD_DESCRIPTIONS: Record<string, string> = {
slug: "stable url-style id",
title: "one-line name",
body: "markdown content",
// The per-kind schema below overrides `status` with its own enum string.
status: "per-kind status enum",
tags: "array of freeform tags",
created_at: "RFC 3339 timestamp",
updated_at: "RFC 3339 timestamp",
};
const PHASE_6_DATA_CLASSIFICATIONS = {
slug: GENERAL,
title: GENERAL,
body: GENERAL,
status: GENERAL,
tags: GENERAL,
created_at: GENERAL,
updated_at: GENERAL,
};
function phase6Schema(
descriptive_name: string,
purpose_statement: string,
statuses: readonly string[],
): AddSchemaRequest {
return {
schema: {
name: descriptive_name,
owner_app_id: OWNER_APP_ID,
descriptive_name,
purpose_statement,
schema_type: "Hash",
key: { hash_field: "slug" },
fields: [...PHASE_6_FIELDS],
field_types: { ...PHASE_6_FIELD_TYPES },
field_descriptions: {
...PHASE_6_FIELD_DESCRIPTIONS,
status: statuses.join("|"),
},
field_classifications: { title: ["word"], body: ["word"] },
field_data_classifications: { ...PHASE_6_DATA_CLASSIFICATIONS },
},
mutation_mappers: {},
};
}
export const designSchema: AddSchemaRequest = phase6Schema(
"Design",
"Design",
DESIGN_STATUSES,
);
export const taskSchema: AddSchemaRequest = {
schema: {
name: "Task",
owner_app_id: OWNER_APP_ID,
descriptive_name: "Task",
purpose_statement: "Task",
schema_type: "Hash",
key: { hash_field: "slug" },
fields: [
"slug",
"title",
"body",
"status",
"design_slug",
"tags",
"created_at",
"updated_at",
],
field_types: {
slug: "String",
title: "String",
body: "String",
status: "String",
design_slug: "String",
tags: { Array: "String" },
created_at: "String",
updated_at: "String",
},
field_descriptions: {
slug: "stable url-style id",
title: "one-line name",
body: "description/notes",
status: TASK_STATUSES.join("|"),
design_slug: "parent Design slug, empty string if none",
tags: "array of freeform tags",
created_at: "RFC 3339 timestamp",
updated_at: "RFC 3339 timestamp",
},
field_classifications: { title: ["word"], body: ["word"] },
field_data_classifications: {
slug: GENERAL,
title: GENERAL,
body: GENERAL,
status: GENERAL,
design_slug: GENERAL,
tags: GENERAL,
created_at: GENERAL,
updated_at: GENERAL,
},
},
mutation_mappers: {},
};
// Per-kind schemas, structurally identical (7 fields) and distinguished
// solely by descriptive_name + purpose_statement. This is retained for
// fbrain's existing catalog identities; new apps should use schema starter
// templates plus a `kind` discriminator per
// decision-2026-07-16-schema-identity-system-treatment.
export const conceptSchema: AddSchemaRequest = phase6Schema(
"Concept",
"Reusable framework, pattern, or protocol recorded for cross-session reuse",
CONCEPT_STATUSES,
);
export const preferenceSchema: AddSchemaRequest = phase6Schema(
"Preference",
"User-stated directive applied across future decisions",
PREFERENCE_STATUSES,
);
export const referenceSchema: AddSchemaRequest = phase6Schema(
"Reference",
"Pointer to an external resource useful for future lookup",
REFERENCE_STATUSES,
);
export const agentSchema: AddSchemaRequest = phase6Schema(
"Agent",
"Persistent assistant identity with role and behavior conventions",
AGENT_STATUSES,
);
export const projectSchema: AddSchemaRequest = phase6Schema(
"Project",
"Active in-flight feature work tracked over its lifecycle",
PROJECT_STATUSES,
);
export const spikeSchema: AddSchemaRequest = phase6Schema(
"Spike",
"Time-boxed investigation or exploration with a defined conclusion",
SPIKE_STATUSES,
);
export const sopSchema: AddSchemaRequest = phase6Schema(
"Sop",
"Standard operating procedure: a repeatable step-by-step process an agent follows to perform a recurring task",
SOP_STATUSES,
);
// Decision gets a DEDICATED shape (not the shared 7-field envelope): the
// whole point of promoting decisions out of the monolithic `decisions-log`
// is to make them queryable, so the things you filter/sort by are real
// columns β `program`, `gate_slug`, `decided_by`, `decided_on` β not buried
// in prose or tags. LastDB stores arbitrary schema fields fine; the extra
// columns are plumbed through fbrain's generic record path via
// `RecordTypeDef.extraStringFields`. The distinct field shape also keeps its
// canonical hash separate from every other type without relying on the
// dual-signal purpose gate.
export const decisionSchema: AddSchemaRequest = {
schema: {
name: "Decision",
owner_app_id: OWNER_APP_ID,
descriptive_name: "Decision",
purpose_statement:
"A call a human made β the choice, its rationale, and outcome β kept as an auditable trail",
schema_type: "Hash",
key: { hash_field: "slug" },
fields: [
"slug",
"title",
"body",
"status",
"program",
"gate_slug",
"decided_by",
"decided_on",
"tags",
"created_at",
"updated_at",
],
field_types: {
slug: "String",
title: "String",
body: "String",
status: "String",
program: "String",
gate_slug: "String",
decided_by: "String",
decided_on: "String",
tags: { Array: "String" },
created_at: "String",
updated_at: "String",
},
field_descriptions: {
slug: "stable url-style id",
title: "one-line decision summary",
body: "rationale, evidence, and context",
status: DECISION_STATUSES.join("|"),
program: "owning program / North Star slug (empty string if none)",
gate_slug: "open-decisions gate this clears (empty string if none)",
decided_by: "who made the call (e.g. Tom)",
decided_on: "RFC 3339 date the decision was made",
tags: "array of freeform tags",
created_at: "RFC 3339 timestamp",
updated_at: "RFC 3339 timestamp",
},
field_classifications: { title: ["word"], body: ["word"] },
field_data_classifications: {
slug: GENERAL,
title: GENERAL,
body: GENERAL,
status: GENERAL,
program: GENERAL,
gate_slug: GENERAL,
decided_by: GENERAL,
decided_on: GENERAL,
tags: GENERAL,
created_at: GENERAL,
updated_at: GENERAL,
},
},
mutation_mappers: {},
};
// Papercut gets a DEDICATED shape for the same reason `decision` does: the
// things you filter, dedupe and count by have to be real columns. The prose
// ledger this replaces stored all of them in freeform body text, and every
// measured bookkeeping failure traced back to that one fact.
//
// `component` is the load-bearing one. The prose ledger scoped a family by
// SLUG PREFIX β every "independent" enumerator ran `grep -o
// "papercut-lastgit-[a-z0-9-]*"` β so on the axis of the prefix they were one
// reader, and a defect filed under any other slug was invisible to all of
// them. Measured 2026-08-06: at least 9 active LastGit defect records sat
// outside the prefix, including a P1. A queryable column cannot be evaded by
// naming a record badly.
//
// `symptom_hash` is the dedupe key: a content hash over
// (component, normalized symptom), so a second run filing the same observable
// collides on write instead of two hours later in a human's reading.
export const papercutSchema: AddSchemaRequest = {
schema: {
name: "Papercut",
owner_app_id: OWNER_APP_ID,
descriptive_name: "Papercut",
purpose_statement:
"One observed defect in our own tooling, with its evidence, repair state and the live check that confirmed it gone",
schema_type: "Hash",
key: { hash_field: "slug" },
fields: [
"slug",
"title",
"body",
"status",
"component",
"repo",
"severity",
"kind",
"symptom_hash",
"fixed_by",
"verified_by",
"duplicate_of",
"tags",
"created_at",
"updated_at",
],
field_types: {
slug: "String",
title: "String",
body: "String",
status: "String",
component: "String",
repo: "String",
severity: "String",
kind: "String",
symptom_hash: "String",
fixed_by: "String",
verified_by: "String",
duplicate_of: "String",
tags: { Array: "String" },
created_at: "String",
updated_at: "String",
},
field_descriptions: {
slug: "stable url-style id",
title: "one-line statement of the defect",
body: "symptom, evidence, reproduction, and the proposed or applied repair",
status: PAPERCUT_STATUSES.join("|"),
component:
"subsystem the defect lives in (lastdb | lastgit | kanban | brain | routines | β¦) β the queryable replacement for the slug-prefix family",
repo: "owning repo as a bare owner/name token (empty string if none)",
severity: PAPERCUT_SEVERITIES.join("|"),
kind: PAPERCUT_KINDS.join("|"),
symptom_hash:
"content hash over (component, normalized symptom) β the dedupe key checked on file",
fixed_by:
"the change that repaired it, e.g. EdgeVector/lastgit #242 (empty string until fixed)",
verified_by:
"the LIVE check that confirmed it gone β never the word 'merged' (empty string until verified)",
duplicate_of:
"slug of the papercut this one duplicates (empty string if none)",
tags: "array of freeform tags",
created_at: "RFC 3339 timestamp",
updated_at: "RFC 3339 timestamp",
},
field_classifications: { title: ["word"], body: ["word"] },
field_data_classifications: {
slug: GENERAL,
title: GENERAL,
body: GENERAL,
status: GENERAL,
component: GENERAL,
repo: GENERAL,
severity: GENERAL,
kind: GENERAL,
symptom_hash: GENERAL,
fixed_by: GENERAL,
verified_by: GENERAL,
duplicate_of: GENERAL,
tags: GENERAL,
created_at: GENERAL,
updated_at: GENERAL,
},
},
mutation_mappers: {},
};
export const RECORD_TYPES = [
"design",
"task",
"concept",
"preference",
"reference",
"agent",
"project",
"spike",
"sop",
"decision",
"papercut",
] as const;
export type RecordType = (typeof RECORD_TYPES)[number];
export function recordTypeList(separator = " | "): string {
return RECORD_TYPES.join(separator);
}
export function recordTypeCount(): number {
return RECORD_TYPES.length;
}
// Internal tag secondary index. This is intentionally NOT a RecordType: it is
// registered and stored in config like other fbrain schemas, but never appears
// on user-facing list/get/search surfaces.
export const TAG_INDEX_SCHEMA_KEY = "__tagindex__";
export const ADMIN_SNAPSHOT_SCHEMA_KEY = "__admin_snapshot__";
// File attachments (`fbrain attach` / `attachments` / `detach` /
// `attachment get`). Two internal support schemas, NOT RecordTypes.
// Attachments stay ADDITIVE by design: registering these schemas changes no
// existing schema's identity hash and migrates no existing record β a
// deliberate contrast to `migrate --add-field`, which re-puts every record
// under a new hash. See docs/attachments.md.
export const ATTACHMENT_INDEX_SCHEMA_KEY = "__attachmentindex__";
export const ATTACHMENT_BLOB_SCHEMA_KEY = "__attachmentblob__";
export const ATTACHMENT_FILE_SCHEMA_KEY = "__attachmentfile__";
export const tagIndexSchema: AddSchemaRequest = {
schema: {
name: "TagIndex",
owner_app_id: OWNER_APP_ID,
descriptive_name: "TagIndex",
purpose_statement:
"Inverted index mapping a tag to the records that carry it, maintained by fbrain to make tag-filtered reads scale with tag cardinality instead of corpus size",
schema_type: "Hash",
key: { hash_field: "slug" },
fields: ["slug", "tag", "members", "created_at", "updated_at"],
field_types: {
slug: "String",
tag: "String",
members: { Array: "String" },
created_at: "String",
updated_at: "String",
},
field_descriptions: {
slug: "reserved __tagidx__<sha256(tag)> key",
tag: "the indexed tag value",
members: "array of type:slug entries carrying this tag",
created_at: "RFC 3339 timestamp",
updated_at: "RFC 3339 timestamp",
},
// NOT SEARCHABLE β every field, deliberately.
//
// TagIndex is brain's own bookkeeping: a machine key
// (`__tagidx__<sha256>`) pointing at a list of `type:slug` members. Nobody
// searches for it, and it is not prose.
//
// Measured 2026-08-08 on the live semantic plane, an UNSCOPED query
// returned this and nothing else in its top four:
//
// 0.747 TagIndex __tagidx__8d8ea229865deee0e8e6dec4b3cf6d6e34β¦
// 0.744 TagIndex __tagidx__4d0cf51fab3231323f683f788c36c3f62dβ¦
// 0.728 TagIndex __tagidx__b2fc4e7094dad451b55e8059bab659f7dbβ¦
//
// A bag of tag tokens lands mid-similarity against almost any query, so
// these crowded the head of every unscoped result. brain's own reads scope
// by type and were unaffected β but "any app can search its own records"
// means new callers arrive, and the first thing a new caller does is an
// unscoped query.
//
// Leaving `field_classifications` unset was not neutral: the host emits
// `searchable_fields: null` for it, which a consumer must read as "legacy,
// unspecified" rather than as exclusion. Only the owning app can say this,
// and saying it explicitly is the whole mechanism β so every field is
// enumerated, and there is deliberately no `word` field. Same shape as the
// attachment support schemas above.
field_classifications: {
slug: ["no_index", "metadata"],
tag: ["no_index", "metadata"],
members: ["no_index", "metadata"],
created_at: ["no_index", "metadata"],
updated_at: ["no_index", "metadata"],
},
field_data_classifications: {
slug: GENERAL,
tag: GENERAL,
members: GENERAL,
created_at: GENERAL,
updated_at: GENERAL,
},
},
mutation_mappers: {},
};
export const adminSnapshotSchema: AddSchemaRequest = {
schema: {
name: "BrainAdminSnapshot",
owner_app_id: OWNER_APP_ID,
descriptive_name: "BrainAdminSnapshot",
purpose_statement:
"Privacy-safe fbrain admin dashboard rollup for delivery as a LastDB slice; stores counts and short summaries only, never full brain bodies or secrets",
schema_type: "Hash",
key: { hash_field: "slug" },
fields: [
"slug",
"source_app",
"schema_version",
"captured_at",
"type_counts_json",
"open_decisions_json",
"active_programs_head_json",
"recent_heartbeats_json",
],
field_types: {
slug: "String",
source_app: "String",
schema_version: "String",
captured_at: "String",
type_counts_json: "String",
open_decisions_json: "String",
active_programs_head_json: "String",
recent_heartbeats_json: "String",
},
field_descriptions: {
slug: "stable snapshot record id, normally admin-brain-snapshot",
source_app: "producer app id",
schema_version: "snapshot payload schema version",
captured_at: "RFC 3339 capture timestamp",
type_counts_json: "JSON object of live record counts by fbrain type",
open_decisions_json: "JSON array of open decision slugs and titles only",
active_programs_head_json:
"JSON array containing a short active-programs rollup head",
recent_heartbeats_json:
"JSON array of recent heartbeat ids, timestamps, and outcomes",
},
field_data_classifications: {
slug: GENERAL,
source_app: GENERAL,
schema_version: GENERAL,
captured_at: GENERAL,
type_counts_json: GENERAL,
open_decisions_json: GENERAL,
active_programs_head_json: GENERAL,
recent_heartbeats_json: GENERAL,
},
},
mutation_mappers: {},
};
// One BrainAttachmentIndex record per (record_type, record_slug) that has
// attachments β the same shape of internal secondary index as TagIndex.
// `filenames` is the ONLY searchable surface (classified ["word"]); the
// `attachments_json` entries carry blob refs + metadata and are explicitly
// no_index, so nothing about attachment content leaks into BM25/vector search.
export const attachmentIndexSchema: AddSchemaRequest = {
schema: {
name: "BrainAttachmentIndex",
owner_app_id: OWNER_APP_ID,
descriptive_name: "BrainAttachmentIndex",
purpose_statement:
"Per-record list of file attachments on an fbrain knowledge record; filename metadata is searchable, attachment content is not",
schema_type: "Hash",
key: { hash_field: "slug" },
fields: [
"slug",
"record_type",
"record_slug",
"filenames",
"attachments_json",
"created_at",
"updated_at",
],
field_types: {
slug: "String",
record_type: "String",
record_slug: "String",
filenames: { Array: "String" },
attachments_json: "String",
created_at: "String",
updated_at: "String",
},
field_descriptions: {
slug: "reserved __attidx__<sha256(type:slug)> key",
record_type: "fbrain record type the attachments belong to",
record_slug: "fbrain record slug the attachments belong to",
filenames: "attachment filenames β the only word-indexed surface",
attachments_json:
"JSON array of {name, blob_ref, size, media_type, added_at} entries",
created_at: "RFC 3339 timestamp",
updated_at: "RFC 3339 timestamp",
},
// Explicit classifications for EVERY field: when field_classifications is
// non-empty the node indexes ONLY fields classified "word" (and never
// secret/no_index ones) β fold_db mutation_manager
// `searchable_native_index_fields`. Filenames in, everything else out.
field_classifications: {
slug: ["no_index", "metadata"],
record_type: ["no_index", "metadata"],
record_slug: ["no_index", "metadata"],
filenames: ["word"],
attachments_json: ["no_index", "metadata"],
created_at: ["no_index", "metadata"],
updated_at: ["no_index", "metadata"],
},
field_data_classifications: {
slug: GENERAL,
record_type: GENERAL,
record_slug: GENERAL,
filenames: GENERAL,
attachments_json: GENERAL,
created_at: GENERAL,
updated_at: GENERAL,
},
},
mutation_mappers: {},
};
// Content-addressed attachment bytes, keyed by SHA-256 of the raw file. This
// is the same "dev-node stand-in for the app-scoped CAS blob plane" pattern
// lastgit uses (LastgitPackBlob): Mini/lastdbd's `/api/app/blob/cas/sha256/*`
// routes are structurally absent (content-free 404), so v1 stores base64
// bytes in a no_index record field. The blob plane is a storage detail hidden
// behind src/attachments.ts β when the node grows real CAS routes (fold
// docs/designs/cloud-file-blobs-on-demand-sync.md P1/P2), only that module
// changes. `embedding: "never"` + no_index/binary keep the bytes out of every
// search index by construction.
export const attachmentBlobSchema: AddSchemaRequest = {
schema: {
name: "BrainAttachmentBlob",
owner_app_id: OWNER_APP_ID,
descriptive_name: "BrainAttachmentBlob",
purpose_statement:
"Content-addressed file bytes backing fbrain attachments; binary payload intentionally excluded from all search indexes",
schema_type: "Hash",
key: { hash_field: "content_hash" },
fields: ["content_hash", "size", "media_type", "data", "created_at"],
field_types: {
content_hash: "String",
size: "String",
media_type: "String",
data: "String",
created_at: "String",
},
field_descriptions: {
content_hash: "SHA-256 hex of the raw file bytes (CAS key)",
size: "raw file byte length",
media_type: "MIME type inferred at attach time",
data: "base64 file bytes; never indexed, never embedded",
created_at: "RFC 3339 timestamp",
},
field_classifications: {
content_hash: ["no_index", "metadata"],
size: ["no_index", "metadata"],
media_type: ["no_index", "metadata"],
data: ["no_index", "binary"],
created_at: ["no_index", "metadata"],
},
field_data_classifications: {
content_hash: GENERAL,
size: GENERAL,
media_type: GENERAL,
data: { sensitivity_level: 0, data_domain: "binary" },
created_at: GENERAL,
},
},
mutation_mappers: {},
};
// v2 attachment storage: one BrainAttachmentFile record per unique content
// hash. The raw bytes do not live in this record; the node's file-blob plane
// stores the encrypted CAS blob and returns a $lastdb_file pointer in `file`.
export const attachmentFileSchema: AddSchemaRequest = {
schema: {
name: "BrainAttachmentFile",
owner_app_id: OWNER_APP_ID,
descriptive_name: "BrainAttachmentFile",
purpose_statement:
"Content-addressed $lastdb_file pointer backing an fbrain attachment; bytes live in the encrypted B2 CAS file plane, never in this record",
schema_type: "Hash",
key: { hash_field: "content_hash" },
fields: ["content_hash", "size", "media_type", "file", "created_at"],
field_types: {
content_hash: "String",
size: "String",
media_type: "String",
file: "Any",
created_at: "String",
},
field_descriptions: {
content_hash: "SHA-256 hex of the raw file bytes (CAS key)",
size: "raw file byte length",
media_type: "MIME type inferred at attach time",
file: "$lastdb_file pointer written by the node's file-blob plane; never indexed",
created_at: "RFC 3339 timestamp",
},
field_classifications: {
content_hash: ["no_index", "metadata"],
size: ["no_index", "metadata"],
media_type: ["no_index", "metadata"],
file: ["no_index", "metadata"],
created_at: ["no_index", "metadata"],
},
field_data_classifications: {
content_hash: GENERAL,
size: GENERAL,
media_type: GENERAL,
file: { sensitivity_level: 0, data_domain: "file-reference" },
created_at: GENERAL,
},
},
mutation_mappers: {},
};
/**
* One fbrain record as a single HashRange row: (rle_h = record type) Γ
* (rle_r = slug). This is the PRODUCT shape for list / BM25 corpus loading.
*
* Why: the legacy rollup holds every record of a type β bodies included β in
* ONE atom, read-modify-written in full on every put. Measured on the primary
* 2026-07-28 at 446,262 B (6.8Γ the 64 KiB product default, 85% of the raised
* ceiling). Crossing the ceiling does not fail the oversized write cleanly β
* it half-commits: the record lands and the index patch is rejected, which is
* how `situations notices` silently staled for hours on 2026-07-27. One row
* per record makes a put O(1) bytes and bounds each atom by ONE record.
*
* Field names MUST stay opaque (`rle_*`). Schema Service field-unifies
* semantic names (slug/title/body/status/β¦) into an existing record identity;
* opaque keys plus one payload string mint a novel HashRange identity. Proved
* for `LastgitPackInventory` 2026-07-25 β see
* `reference-lastgit-pack-inventory-hashrange-cutover`. The
* `_hashrange_v2` descriptive-name suffix and the `layout` marker are part of
* that novelty; do not "tidy" them away.
*/
export const RECORD_LIST_ENTRY_SCHEMA_KEY = "__recordlistentry__";
export const RECORD_LIST_ENTRY_MARKER = "fbrain_record_list_entry_v1";
export const RECORD_LIST_ENTRY_LAYOUT =
"RecordListEntry novel hashrange rle_h x rle_r";
/**
* Reserved range key marking "this type's partition holds every record of the
* type" β i.e. legacy has been drained into it. A slug can never collide with
* it: record slugs are kebab-case and never contain `__`.
*
* Without this marker a non-empty partition is AMBIGUOUS β fully migrated, or
* one freshly-put row while the other 300 records still sit in legacy. Reading
* "HashRange wins whenever non-empty" resolves that ambiguity the wrong way and
* silently truncates the type to the records written since the cutover, which
* hits `brain list` AND the BM25 corpus behind `brain ask`.
*/
export const RECORD_LIST_ENTRY_MIGRATED_RANGE = "__rle_migrated__";
export const RECORD_LIST_ENTRY_FIELDS = [
"rle_h",
"rle_r",
"rle_payload",
"rle_marker",
"layout",
] as const;
export const recordListEntrySchema: AddSchemaRequest = {
schema: {
name: "RecordListEntry",
owner_app_id: OWNER_APP_ID,
descriptive_name: "RecordListEntry_hashrange_v2",
purpose_statement:
"One fbrain record per HashRange row (type partition x slug) so list and BM25 read a keyed partition instead of a single full-corpus rollup atom that is rewritten on every put",
schema_type: "HashRange",
key: { hash_field: "rle_h", range_field: "rle_r" },
fields: [...RECORD_LIST_ENTRY_FIELDS],
field_types: {
rle_h: "String",
rle_r: "String",
rle_payload: "String",
rle_marker: "String",
layout: "String",
},
field_descriptions: {
rle_h: "opaque list partition token (record type name value)",
rle_r: "opaque list range token (record slug value)",
rle_payload: "json object of ONE fbrain record (not an array of records)",
rle_marker: "constant token fbrain_record_list_entry_v1",
layout: RECORD_LIST_ENTRY_LAYOUT,
},
field_classifications: {
rle_h: ["no_index", "metadata"],
rle_r: ["no_index", "metadata"],
rle_payload: ["no_index", "metadata"],
rle_marker: ["no_index", "metadata"],
layout: ["no_index", "metadata"],
},
field_data_classifications: {
rle_h: GENERAL,
rle_r: GENERAL,
rle_payload: GENERAL,
rle_marker: GENERAL,
layout: GENERAL,
},
},
mutation_mappers: {},
};
/**
* One live task per HashRange row, addressed by (ctd_h = design_slug) x
* (ctd_r = task slug). Lets `findChildTasksByDesign` / the delete cascade
* guard point-read one design's children (`{HashKey: designSlug}`) instead
* of reading the WHOLE task partition via `listRecords` and filtering by
* `design_slug` in the client β the same "list everything, filter locally"
* shape RecordListEntry replaced, one layer up: task partition size no
* longer bounds a single-design lookup.
*
* Same opaque-field-names shape as `recordListEntrySchema` (`ctd_*` instead
* of `rle_*`) so Schema Service mints a novel HashRange identity instead of
* field-unifying into an existing one β see the comment on
* `recordListEntrySchema` for why that matters.
*
* A single reserved GLOBAL marker row (`CHILD_TASK_INDEX_GLOBAL_HASH` /
* `CHILD_TASK_INDEX_MIGRATED_RANGE`) β not a per-design marker β records
* "this index reflects every live task's design_slug". One marker suffices
* because every write path patches its OWN row incrementally from the
* moment the schema is registered; only the historical backfill (every task
* that existed before registration) needs the bulk `fbrain reindex
* --child-task-index` rebuild, and that rebuild covers all designs in one
* pass and stamps one marker when done.
*/
export const CHILD_TASK_INDEX_SCHEMA_KEY = "__childtaskindex__";
export const CHILD_TASK_INDEX_MARKER = "fbrain_child_task_index_v1";
/**
* Reserved hash partition for the global completeness marker. Design slugs
* are kebab-case and never contain `__`, so this can never collide with a
* real design's partition.
*/
export const CHILD_TASK_INDEX_GLOBAL_HASH = "__ctd_global__";
export const CHILD_TASK_INDEX_MIGRATED_RANGE = "__ctd_migrated__";
export const CHILD_TASK_INDEX_FIELDS = [
"ctd_h",
"ctd_r",
"ctd_payload",
"ctd_marker",
] as const;
export const childTaskIndexSchema: AddSchemaRequest = {
schema: {
name: "ChildTaskIndex",
owner_app_id: OWNER_APP_ID,
descriptive_name: "ChildTaskIndex_hashrange_v1",
purpose_statement:
"One live task per HashRange row (design_slug partition x task slug) so a design's children resolve via a keyed partition read instead of listing every task and filtering by design_slug in the client",
schema_type: "HashRange",
key: { hash_field: "ctd_h", range_field: "ctd_r" },
fields: [...CHILD_TASK_INDEX_FIELDS],
field_types: {
ctd_h: "String",
ctd_r: "String",
ctd_payload: "String",
ctd_marker: "String",
},
field_descriptions: {
ctd_h:
"opaque partition token (parent design slug value, or the reserved global-marker hash)",
ctd_r:
"opaque range token (child task slug value, or the reserved migrated-marker range)",
ctd_payload:
"json object of ONE fbrain task record (not an array), empty for the marker row",
ctd_marker: "constant token fbrain_child_task_index_v1",
},