@tpsdev-ai/flair 0.22.0 → 0.23.0

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.
@@ -0,0 +1,363 @@
1
+ /**
2
+ * record-types.ts — the RecordType registry (record-types slice 2, flair#520).
3
+ *
4
+ * ─── What this is ────────────────────────────────────────────────────────
5
+ * The declared, PR-reviewed policy layer over resources/record-type-kit.ts
6
+ * (slice 1, flair#729): a static `RECORD_TYPES` map naming, for every
7
+ * flair-owned table, WHICH read-scope model it uses, WHICH no-forge
8
+ * attribution idiom it stamps on writes, whether it stamps provenance,
9
+ * whether it carries an embedding column, and whether it participates in
10
+ * federation — the five capabilities the flair#520 design draft's §4 laid
11
+ * out (identity / read-scope / provenance / semantic recall / REM), plus
12
+ * the attribution split slice 1 surfaced (four named idioms, not one) and
13
+ * the federation/MCP-shape refinements Kern and Sherlock's DESIGN REVIEW
14
+ * landed on the issue thread.
15
+ *
16
+ * This module is DATA, not a framework: `RecordTypePolicy` is a plain
17
+ * interface, `RECORD_TYPES` is a plain object literal. It does not derive
18
+ * MCP tools (that is slice 3 — see the `mcp` field's own doc below for why
19
+ * its SHAPE lands now without any consumer reading it yet), does not wire
20
+ * embeddings (Memory's embedding logic stays exactly where slice 1 left it
21
+ * — dedup-gate-entangled inline code in resources/Memory.ts, not routed
22
+ * through this registry), and does not change what Federation.ts/src/cli.ts
23
+ * actually sync (their hardcoded table lists are untouched — see the
24
+ * `federation` field's doc). Every field on every one of the five entries
25
+ * below documents CURRENT shipped behavior; none of them CHANGE it.
26
+ *
27
+ * ─── The static-registry invariant (non-negotiable) ───────────────────────
28
+ * Per §3 of the flair#520 design draft and Sherlock/Kern's unanimous
29
+ * verdict: this registry is STATIC, COMPILED, and PR-REVIEWED — never
30
+ * runtime-populated. A new entry lands via a PR to THIS file, same trust
31
+ * level as an entry in resources/mcp-tools.ts's TOOLS map. No component,
32
+ * consumer, or plugin can add itself to `RECORD_TYPES` at boot. This is the
33
+ * structural rejection of the exact failure mode Sherlock's review closed
34
+ * for cause on flair#541: Harper's native-MCP application profile
35
+ * auto-exposing every resource as a tool, with no allow/deny, at runtime.
36
+ * A dynamic `RECORD_TYPES` would reproduce that failure one layer up. If
37
+ * this ever needs to become an out-of-tree-extensible SDK (flair#520 §9
38
+ * calls that "a plausible later shape, not v1"), the review bar for that
39
+ * relaxation must be at least as high as the original #520/#541 decision.
40
+ *
41
+ * ─── Field-by-field rationale ─────────────────────────────────────────────
42
+ *
43
+ * `identity` — always "gated" for all five entries today. `makeAuthGate()`
44
+ * (record-type-kit.ts) only implements the "gated" posture (resolveAgentAuth
45
+ * + allowVerified — verified agents, admins, and trusted internal calls
46
+ * pass; anonymous HTTP is denied). "internal-only" is reserved for a future
47
+ * system table with no agent-facing surface at all — nothing wires it yet;
48
+ * a table setting it today would be a registry error, not a live capability.
49
+ *
50
+ * `readScope` — three values, not the two the original §4 draft proposed.
51
+ * "owner-only" and "open-within-org" are the two K&S-approved models
52
+ * (Relationship/WorkspaceState vs. Memory). The THIRD value, "none", names
53
+ * a real state that already ships and that record-type-kit.ts's own file
54
+ * header calls out explicitly: OrgEvent and Soul have NO get()/search()
55
+ * override at all — any verified agent (past the identity gate) reads
56
+ * every row, unscoped by owner, with no visibility field in play. That is
57
+ * neither "owner-only" (it is strictly broader — no ownership filter at
58
+ * all) nor "open-within-org" (there is no visibility-based private
59
+ * exception — nothing is excluded). Labeling OrgEvent/Soul "owner-only" to
60
+ * force them into the K&S-approved binary would be a FALSE registry entry —
61
+ * exactly the kind of drift a policy registry exists to prevent, and worse
62
+ * than just leaving the gap undocumented. This is a deliberate, disclosed
63
+ * extension of the flair#520 design draft's §4 shape (flagged in the
64
+ * slice-2 report for K&S to weigh in on), not a silent reinterpretation of
65
+ * their "owner-only | open-within-org" verdict.
66
+ *
67
+ * `readScope` narrowing is a BREAKING CHANGE, not covered by the
68
+ * additive-only discipline the design draft's §8 states for every other
69
+ * field. Per Sherlock's DESIGN REVIEW comment (explicitly concurred by
70
+ * Kern): flipping `open-within-org` → `owner-only`, or `none` → either
71
+ * narrower model, would retroactively hide rows other agents could already
72
+ * read — the same class of scrutiny as any other access-narrowing decision,
73
+ * called out separately here because it is easy to mis-file as "just
74
+ * editing a registry constant." WIDENING (owner-only → open-within-org, or
75
+ * either → none) is a normal, reviewable capability-addition PR. NARROWING
76
+ * requires the review bar of a genuine access-removal change: confirm
77
+ * nothing currently depends on the wider read, and treat it with the same
78
+ * weight as shipping a new private-visibility default.
79
+ *
80
+ * Review-gate tiering (Kern/Sherlock DESIGN REVIEW, Q1): a new entry with
81
+ * `readScope: "owner-only"` ships on the standard architecture-review bar.
82
+ * `readScope: "open-within-org"` (or, by the same logic, `"none"` — an even
83
+ * wider grant than open-within-org for records with no visibility concept
84
+ * at all) requires Sherlock's explicit security sign-off on the read-scope
85
+ * decision specifically, the same scrutiny Memory's own visibility work
86
+ * got. This registry's job is to make that decision a single reviewable
87
+ * line per type; the review-bar tiering itself is a process discipline for
88
+ * whoever reviews the PR, not something this module enforces in code.
89
+ *
90
+ * `attribution` — the four named `AttributionMode` idioms from
91
+ * record-type-kit.ts (`validate-truthy` / `validate-strict` /
92
+ * `stamp-default` / `stamp-strict`), keyed per WRITE METHOD
93
+ * (`post`/`put`), not one mode per type. This is the concrete thing slice 1
94
+ * surfaced that the original §4 draft did not anticipate: WorkspaceState
95
+ * and OrgEvent use a DIFFERENT idiom on post() (`stamp-default` — silent
96
+ * unconditional overwrite, never rejects) than on put()
97
+ * (`validate-strict` — rejects a mismatch, including an absent field).
98
+ * Collapsing that to one type-level `attributionMode` field would either be
99
+ * wrong for one of the two methods or would require silently converging
100
+ * post() onto put()'s stricter idiom — a real behavior change flagged but
101
+ * explicitly deferred by Flint's post-slice-1 comment on #520 ("Sherlock
102
+ * flagging stamp-default → stamp-strict convergence... Registry slice
103
+ * follows" — convergence is a FUTURE decision, not made here). Each
104
+ * sub-field is optional because not every table's write path calls
105
+ * `stampAttribution` on both methods: Relationship has no post() override
106
+ * at all (only `put`), so `attribution.post` is absent, not a placeholder.
107
+ * The stamped attribute name is always `ownerField` above — no type uses a
108
+ * different field for attribution vs. ownership scoping.
109
+ *
110
+ * `provenance` — true only for Memory and Relationship (the two tables that
111
+ * call `buildProvenance()` today and carry a nullable `provenance: String`
112
+ * schema field). WorkspaceState/OrgEvent/Soul are `false`: not an oversight
113
+ * — nobody wired it, and none of the three declare the schema field, so
114
+ * flipping this to `true` without a schema migration would be a lie the
115
+ * registry tells about what actually gets stamped.
116
+ *
117
+ * `embedding` — set only for Memory (`content` is the embedded field; the
118
+ * type has its own exposed semantic-search tool today — `memory_search` /
119
+ * resources/SemanticSearch.ts — hence `exposedSearch: true`). Declared for
120
+ * documentation ONLY in this slice: Memory's actual embedding logic stays
121
+ * exactly where slice 1's kit-extraction doc said it would stay — inline in
122
+ * resources/Memory.ts, dedup-gate-entangled, NOT routed through this
123
+ * registry or through a `stampEmbedding()` kit helper. Wiring is a later
124
+ * slice's job; this field exists so that slice doesn't need a breaking
125
+ * schema change to the policy shape when it lands (same reserved-flag
126
+ * pattern as `remEligible`).
127
+ *
128
+ * `remEligible` — MUST be `false` for every entry in v1 (typed as the
129
+ * literal `false`, not `boolean`, so a stray `true` is a compile error, not
130
+ * just a runtime policy mistake). REM's nightly gather step
131
+ * (specs/FLAIR-NIGHTLY-REM.md §11) is hardcoded to `GET /Memory?agentId=…`
132
+ * — single table, single agent, by construction. There is no multi-table
133
+ * distillation input path to opt into yet; the field exists so a future
134
+ * REM generalization doesn't require a breaking schema change to every
135
+ * existing entry.
136
+ *
137
+ * `federation` — "included" or "excluded", naming whether the type's rows
138
+ * currently CAN leave this instance via federation sync at all. This is
139
+ * DECLARATIVE ONLY in this slice, same discipline as `embedding` above:
140
+ * Federation.ts's `FederationSync.post()` table map and src/cli.ts's
141
+ * `runFederationSyncOnce`'s push table list are the actual, unchanged,
142
+ * hardcoded source of truth (`["Memory", "Soul", "Agent", "Relationship"]`)
143
+ * — this field documents what that hardcoded list already does per type,
144
+ * it does not drive it. Memory/Relationship/Soul are "included" because
145
+ * they are already in both lists (Memory's push additionally excludes
146
+ * `private`-visibility rows via classifyRecord — a finer-grained rule this
147
+ * boolean-ish field does not attempt to express). WorkspaceState/OrgEvent
148
+ * are "excluded" because they are in neither list today — org events and
149
+ * workspace state never leave the instance.
150
+ *
151
+ * For any FUTURE (non-core, consumer-defined) type, `federation` MUST be
152
+ * `"excluded"` — per Sherlock's DESIGN REVIEW (Q6, Kern concurring):
153
+ * DESIGN.md sequenced Memory's own open-within-org read AFTER federation
154
+ * hardening specifically so within-instance openness wouldn't outrun the
155
+ * one hard cross-instance boundary it depends on. A new type's registration
156
+ * must not silently inherit whatever Federation.ts's push filter does or
157
+ * doesn't already exclude — the filter does not know about new tables at
158
+ * all. Opting a new type into federation requires updating that filter
159
+ * FIRST, in its own reviewed PR, never the other order. This registry does
160
+ * not enforce that sequencing in code (there is no derivation from this
161
+ * field yet — see above); it is a review-time invariant for whoever adds
162
+ * the next entry.
163
+ *
164
+ * `mcp` — DECLARED AND ENFORCED as of slice 3 (flair#520 design review
165
+ * round 2, issue comment 2026-07-18 — Kern APPROVE all four asks, Sherlock
166
+ * APPROVE with one refinement, both unanimous). This field is the reviewed
167
+ * DECLARATION of the MCP surface, not a runtime generator: resources/
168
+ * mcp-tools.ts's hand-written `TOOLS` map stays the actual dispatch table.
169
+ * The slice-3 design round audited all 12 shipped tools and found only 5
170
+ * are simple table-verb wrappers (memory_get/store/delete, soul_get/set —
171
+ * and even the soul pair does bespoke `agentId:key` id synthesis); the rest
172
+ * are composite or bespoke (bootstrap, attention, memory_search,
173
+ * memory_update, record_usage) and cannot be generated from a registry
174
+ * entry without either losing schema/behavior specifics or duplicating the
175
+ * handler. Generating the 5 simple tools would also touch the
176
+ * security-critical `/mcp` dispatch path for zero behavior change, and
177
+ * runtime derivation from a registry is structurally the same failure
178
+ * flair#541 closed for cause (Harper's native-MCP auto-exposing every
179
+ * resource as a tool) one layer up. What declare-and-enforce buys instead:
180
+ * any PR that adds or removes an MCP tool must now also touch a policy
181
+ * chokepoint — either a `RECORD_TYPES.<Table>.mcp` declaration (table-verb
182
+ * tools) or the `COMPOSITE_MCP_TOOLS` allowlist below (everything else) —
183
+ * enforced bidirectionally by test/unit/mcp-surface-tripwire.test.ts, or CI
184
+ * fails. Declaring a verb here does not create a tool; it documents (and,
185
+ * via the tripwire, locks in) a tool resources/mcp-tools.ts already
186
+ * implements.
187
+ *
188
+ * Per Kern's DESIGN REVIEW refinement (Sherlock's Q5, Kern concurring): NOT
189
+ * a flat `verbs` array. `readVerbs` (get/search — same review bar as adding
190
+ * the type itself, since they exercise the same `readScope` the REST
191
+ * surface already exposes) is structurally separate from `writeVerbs`
192
+ * (store/delete/update — a write/delete/update surface over MCP, requiring
193
+ * its own explicit line-item sign-off). `"update"` was added to the
194
+ * `writeVerbs` union in slice 3 — APPROVED by both Kern and Sherlock as
195
+ * documenting `memory_update`'s already-shipped two-branch
196
+ * read-modify-write (in-place overwrite, or a `supersedes`-linked new
197
+ * version), not a new capability.
198
+ *
199
+ * Backfilled on FOUR of the five core entries below, documenting the
200
+ * CURRENT shipped surface exactly (slice-2 discipline: registration, not
201
+ * behavior change) — Memory (`get`/`search` reads; `store`/`delete`/
202
+ * `update` writes), Soul (`get` read; `store` write), WorkspaceState (no
203
+ * reads; `store` write), OrgEvent (no reads; `store` write). Relationship
204
+ * stays `mcp`-absent: it has no MCP tool today, and absent means no
205
+ * exposure, exactly as slice 2 defined.
206
+ *
207
+ * Verb→tool-name mapping (default `${toolPrefix}_${verb}`, with three
208
+ * overrides — `(Soul, store)` → `soul_set`, `(WorkspaceState, store)` →
209
+ * `flair_workspace_set`, `(OrgEvent, store)` → `flair_orgevent`) lives in
210
+ * resources/mcp-tools.ts's `TOOL_NAME_OVERRIDES`, not here. Per Kern/
211
+ * Sherlock's unanimous verdict: the registry declares WHAT is exposed
212
+ * (capability); mcp-tools.ts owns HOW (names, defaults, routing, input
213
+ * schema). Keeping presentation out of this file is what keeps
214
+ * `RecordTypeMcp` a pure capability declaration.
215
+ *
216
+ * See `COMPOSITE_MCP_TOOLS` below for the second — and only other —
217
+ * reviewed chokepoint: tools that are cross-table, aggregate, or otherwise
218
+ * cannot map to a single table + verb (`bootstrap`, `attention`,
219
+ * `record_usage`). Sherlock's slice-3 refinement (Kern concurring on the
220
+ * relocation) put that allowlist HERE, in record-types.ts, rather than in
221
+ * mcp-tools.ts — so the FULL MCP surface (table-verb tools and composites
222
+ * alike) is reviewable in this one file, not split across two.
223
+ *
224
+ * Design lineage: flair#520 design draft (issue comment, 2026-07-13) §4;
225
+ * Sherlock's Security Review (readScope narrowing, mandatory-vs-optional
226
+ * content-safety — NOT added here per Kern's Q3 verdict below —, structural
227
+ * readVerbs/writeVerbs split, federation-excluded default); Kern's DESIGN
228
+ * REVIEW (tiered review gate, readVerbs/writeVerbs, federation-excluded
229
+ * default, provenance-stamp-identical-shape); slice 1 kit extraction
230
+ * (flair#729, merged) whose file header first named the four attribution
231
+ * idioms and the OrgEvent/Soul unscoped-read state this module's `readScope`
232
+ * type makes explicit.
233
+ *
234
+ * Deliberately NOT added in this slice (Kern's DESIGN REVIEW Q3 verdict,
235
+ * explicit): a `contentSafety`/`scanFields` field. Sherlock proposed making
236
+ * content-safety scanning mandatory for any free-text field; Kern's verdict
237
+ * (which this implementation follows, per the task's explicit slice-2 scope)
238
+ * was NOT mandatory in v1 — generalizing `scanFields()` beyond Memory's
239
+ * hand-wired `content`/`summary` call needs its own design pass, and making
240
+ * it a blocking registration requirement would slow the easy win (data-layer
241
+ * -only registration with no MCP exposure). Left as a documented follow-up,
242
+ * not a silently-dropped field.
243
+ */
244
+ // ─── The registry ───────────────────────────────────────────────────────────
245
+ //
246
+ // `as const satisfies Record<string, RecordTypePolicy>`: `satisfies` checks
247
+ // every entry against `RecordTypePolicy` (catching an invalid readScope
248
+ // value, a missing required field, etc. at compile time) WITHOUT widening
249
+ // the expression's inferred type the way a `: Record<string,
250
+ // RecordTypePolicy>` annotation would — so `RECORD_TYPES.Memory.readScope`
251
+ // keeps its literal type `"open-within-org"`, not the widened union
252
+ // `RecordTypeReadScopeMode`. That literal-preservation is what lets the five
253
+ // resource classes below pass `RECORD_TYPES.<Table>.<field>` straight into
254
+ // `makeReadScope()`/`stampAttribution()` (which expect the narrow literal
255
+ // unions, not the wide ones) with no runtime cast or non-null assertion.
256
+ export const RECORD_TYPES = {
257
+ Memory: {
258
+ table: "Memory",
259
+ ownerField: "agentId",
260
+ identity: "gated",
261
+ readScope: "open-within-org",
262
+ attribution: { post: "validate-truthy", put: "validate-truthy" },
263
+ provenance: true,
264
+ embedding: { field: "content", exposedSearch: true },
265
+ remEligible: false,
266
+ federation: "included",
267
+ mcp: { toolPrefix: "memory", readVerbs: ["get", "search"], writeVerbs: ["store", "delete", "update"] },
268
+ },
269
+ Relationship: {
270
+ table: "Relationship",
271
+ ownerField: "agentId",
272
+ identity: "gated",
273
+ readScope: "owner-only",
274
+ // No `post` — Relationship.ts has no post() override at all; only
275
+ // put() (upsert) calls stampAttribution.
276
+ attribution: { put: "stamp-strict" },
277
+ provenance: true,
278
+ remEligible: false,
279
+ federation: "included",
280
+ },
281
+ WorkspaceState: {
282
+ table: "WorkspaceState",
283
+ ownerField: "agentId",
284
+ identity: "gated",
285
+ readScope: "owner-only",
286
+ attribution: { post: "stamp-default", put: "validate-strict" },
287
+ provenance: false,
288
+ remEligible: false,
289
+ federation: "excluded",
290
+ mcp: { toolPrefix: "flair_workspace", readVerbs: [], writeVerbs: ["store"] },
291
+ },
292
+ OrgEvent: {
293
+ table: "OrgEvent",
294
+ ownerField: "authorId",
295
+ identity: "gated",
296
+ // No get()/search() override — any verified agent reads every org
297
+ // event, unscoped. See this module's header doc's `readScope` section.
298
+ readScope: "none",
299
+ attribution: { post: "stamp-default", put: "validate-strict" },
300
+ provenance: false,
301
+ remEligible: false,
302
+ federation: "excluded",
303
+ mcp: { toolPrefix: "flair_orgevent", readVerbs: [], writeVerbs: ["store"] },
304
+ },
305
+ Soul: {
306
+ table: "Soul",
307
+ ownerField: "agentId",
308
+ identity: "gated",
309
+ // No get()/search() override — souls are identity/discovery data,
310
+ // intentionally readable by any verified agent. See header doc.
311
+ readScope: "none",
312
+ attribution: { post: "validate-truthy", put: "validate-truthy" },
313
+ provenance: false,
314
+ remEligible: false,
315
+ federation: "included",
316
+ mcp: { toolPrefix: "soul", readVerbs: ["get"], writeVerbs: ["store"] },
317
+ },
318
+ };
319
+ // ─── Composite MCP tools (the second, and only other, reviewed chokepoint) ─
320
+ //
321
+ // Tools that do not map to a single table + verb — cross-table aggregates,
322
+ // bespoke resources, or multi-source composites. These cannot be expressed
323
+ // via `RECORD_TYPES.<Table>.mcp` (there is no single `table` to attach them
324
+ // to), so slice 3's design round (Sherlock's refinement, Kern concurring —
325
+ // see the `mcp` header doc above) makes this array the SECOND reviewed
326
+ // chokepoint for the MCP surface: test/unit/mcp-surface-tripwire.test.ts
327
+ // requires every tool name in resources/mcp-tools.ts's `TOOLS` map to be
328
+ // either derived from a declared `RECORD_TYPES.<Table>.mcp` verb OR listed
329
+ // here. A PR adding a new composite MCP tool (or renaming/removing one of
330
+ // these three) must touch this array — same review bar as touching a
331
+ // table's `mcp` field.
332
+ //
333
+ // bootstrap — Soul + Memory + predicted-context composite (BootstrapMemories)
334
+ // attention — cross-table aggregate query (AttentionQuery.ts), not a table verb
335
+ // record_usage — usage-signal resource (RecordUsage.ts), not a table verb
336
+ //
337
+ export const COMPOSITE_MCP_TOOLS = ["bootstrap", "attention", "record_usage"];
338
+ // ─── Runtime immutability ───────────────────────────────────────────────────
339
+ // Belt-and-suspenders backstop for the "static, compiled" invariant (see
340
+ // header doc). TypeScript's `as const satisfies` already gives compile-time
341
+ // readonly types, but that alone does not stop a runtime consumer (a
342
+ // resource file, a test, a hypothetical future dynamic-registration attempt)
343
+ // from mutating a nested object in place — `RECORD_TYPES.Memory.readScope =
344
+ // "owner-only"` is a type error at compile time but would silently SUCCEED
345
+ // at runtime without this. Deep-freezing every entry and every nested
346
+ // sub-object (attribution/embedding/mcp) makes an attempted mutation throw
347
+ // (every .ts file in this repo compiles as an ES module, which is always
348
+ // strict mode) instead of silently desyncing the registry from what a
349
+ // resource file already composed at its own module-load time. Same
350
+ // treatment for `COMPOSITE_MCP_TOOLS` — it is the second reviewed MCP-surface
351
+ // chokepoint (see its own doc above) and gets the same static-registry
352
+ // guarantee as RECORD_TYPES itself.
353
+ function deepFreeze(value) {
354
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
355
+ for (const key of Object.getOwnPropertyNames(value)) {
356
+ deepFreeze(value[key]);
357
+ }
358
+ Object.freeze(value);
359
+ }
360
+ return value;
361
+ }
362
+ deepFreeze(RECORD_TYPES);
363
+ deepFreeze(COMPOSITE_MCP_TOOLS);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -58,7 +58,7 @@
58
58
  "@harperfast/oauth": "2.2.0",
59
59
  "@types/js-yaml": "4.0.9",
60
60
  "commander": "14.0.3",
61
- "harper-fabric-embeddings": "^0.3.0",
61
+ "harper-fabric-embeddings": "^0.4.0",
62
62
  "jose": "6.2.2",
63
63
  "js-yaml": "4.1.1",
64
64
  "node-llama-cpp": "3.18.1",
@@ -53,7 +53,7 @@ type Memory @table(database: "flair") {
53
53
  validFrom: String @indexed # ISO timestamp — when this fact became true
54
54
  validTo: String @indexed # ISO timestamp — when this fact stopped being true (null = still valid)
55
55
  _safetyFlags: [String] # content safety flags from scanContent()
56
- provenance: String # JSON blob (memory-provenance slice 1): { v, verified: { agentId, timestamp }, claimed?: { model } }
56
+ provenance: String # JSON blob (memory-provenance slice 1; claimed.client added by flair#718): { v, verified: { agentId, timestamp }, claimed?: { model, client } }
57
57
  # Nullable by design — existing rows read back provenance = null, unchanged behavior (clean-upgrade-path gate).
58
58
  # Same idiom as Soul.metadata below. Stamped server-side in resources/Memory.ts; never client-writable.
59
59
  originatorInstanceId: String @indexed # federation-edge-hardening slice 1: WRITE-TIME instance identity, stamped server-side in
@@ -99,7 +99,7 @@ type Relationship @table(database: "flair") {
99
99
  # for the full contract (write-time stamp, nullable, preserved across sync merges).
100
100
  # Stamped server-side in resources/Relationship.ts's put().
101
101
  provenance: String # JSON blob (relationship-write-path, folded K&S refinement): SAME shape as
102
- # Memory.provenance above — { v, verified: { agentId, timestamp }, claimed?: { model } }
102
+ # Memory.provenance above — { v, verified: { agentId, timestamp }, claimed?: { model, client } }
103
103
  # — via the shared resources/provenance.ts buildProvenance(), never a
104
104
  # Relationship-specific format. Nullable by design — pre-existing rows read back
105
105
  # provenance = null, unchanged behavior (migration-equivalence gate, same