@tpsdev-ai/flair 0.6.0 → 0.6.1
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/bridges/runtime/roundtrip.js +154 -0
- package/dist/cli.js +168 -32
- package/package.json +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round-trip test harness for bridges.
|
|
3
|
+
*
|
|
4
|
+
* A bridge passes the round-trip test iff:
|
|
5
|
+
* 1. Parse the fixture (or descriptor's first import.source.path) → pass1 BridgeMemory[]
|
|
6
|
+
* 2. Apply export.targets[0]: when-filter → map → write to tmp in the
|
|
7
|
+
* target's format
|
|
8
|
+
* 3. Re-import the tmp file using import.sources[0] → pass2 BridgeMemory[]
|
|
9
|
+
* 4. pass1 and pass2 must agree on the round-trip-stable fields from the
|
|
10
|
+
* spec (§8): content, subject, tags, durability.
|
|
11
|
+
*
|
|
12
|
+
* This exercises the full mapper + predicate + writer + parser chain —
|
|
13
|
+
* if a bridge author's descriptor has mapping bugs, the round-trip
|
|
14
|
+
* surfaces them before memories ever reach Flair.
|
|
15
|
+
*
|
|
16
|
+
* Implementation note: does NOT talk to Flair. Fixture-to-fixture only.
|
|
17
|
+
* Lives here so slice-3c code plugins can reuse the same harness.
|
|
18
|
+
*/
|
|
19
|
+
import { promises as fsp } from "node:fs";
|
|
20
|
+
import { join, isAbsolute } from "node:path";
|
|
21
|
+
import { tmpdir } from "node:os";
|
|
22
|
+
import { randomUUID } from "node:crypto";
|
|
23
|
+
import { BridgeRuntimeError } from "../types.js";
|
|
24
|
+
import { parseRecords } from "./formats.js";
|
|
25
|
+
import { applyMap } from "./mapper.js";
|
|
26
|
+
import { evaluatePredicate } from "./predicate.js";
|
|
27
|
+
import { writeRecords } from "./writers.js";
|
|
28
|
+
/** Fields the spec (§8) requires to survive round-trip. */
|
|
29
|
+
export const ROUND_TRIP_STABLE_FIELDS = ["content", "subject", "tags", "durability"];
|
|
30
|
+
export async function runRoundTrip(opts) {
|
|
31
|
+
const { descriptor, cwd } = opts;
|
|
32
|
+
if (!descriptor.import || descriptor.import.sources.length === 0) {
|
|
33
|
+
throw new BridgeRuntimeError({
|
|
34
|
+
bridge: descriptor.name,
|
|
35
|
+
op: "test",
|
|
36
|
+
field: "import",
|
|
37
|
+
expected: "descriptor with at least one import source",
|
|
38
|
+
got: "missing or empty",
|
|
39
|
+
hint: "round-trip needs an import block to start from. Add `import.sources:` to the descriptor.",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (!descriptor.export || descriptor.export.targets.length === 0) {
|
|
43
|
+
throw new BridgeRuntimeError({
|
|
44
|
+
bridge: descriptor.name,
|
|
45
|
+
op: "test",
|
|
46
|
+
field: "export",
|
|
47
|
+
expected: "descriptor with at least one export target",
|
|
48
|
+
got: "missing or empty",
|
|
49
|
+
hint: "round-trip needs an export block to round-trip through. Add `export.targets:` to the descriptor.",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const importSource = descriptor.import.sources[0];
|
|
53
|
+
const exportTarget = descriptor.export.targets[0];
|
|
54
|
+
const resolvedFixture = resolvePath(cwd, opts.fixturePath ?? importSource.path);
|
|
55
|
+
const tmpDir = await fsp.mkdtemp(join(tmpdir(), `flair-bridge-test-${descriptor.name}-`));
|
|
56
|
+
const tmpPath = join(tmpDir, `roundtrip.${suffixForFormat(exportTarget.format)}`);
|
|
57
|
+
// Pass 1: fixture → BridgeMemory[]
|
|
58
|
+
const pass1 = [];
|
|
59
|
+
for await (const { record } of parseRecords(descriptor.name, resolvedFixture, importSource.format)) {
|
|
60
|
+
const mapped = applyMap(importSource.map, record);
|
|
61
|
+
pass1.push(mapped);
|
|
62
|
+
}
|
|
63
|
+
// Filter pass1 by the export target's when: (records filtered out wouldn't make it to the target).
|
|
64
|
+
const expected = [];
|
|
65
|
+
for (const m of pass1) {
|
|
66
|
+
if (!exportTarget.when || exportTarget.when.trim() === "") {
|
|
67
|
+
expected.push(m);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const result = evaluatePredicate(exportTarget.when, m);
|
|
71
|
+
if (result === "match" || result === "unparsable")
|
|
72
|
+
expected.push(m);
|
|
73
|
+
}
|
|
74
|
+
// Export phase: apply export.map, write to tmp
|
|
75
|
+
const shaped = [];
|
|
76
|
+
for (const m of expected) {
|
|
77
|
+
const out = applyMap(exportTarget.map, m);
|
|
78
|
+
if (Object.keys(out).length === 0)
|
|
79
|
+
continue;
|
|
80
|
+
shaped.push(out);
|
|
81
|
+
}
|
|
82
|
+
await writeRecords(descriptor.name, tmpPath, exportTarget.format, shaped);
|
|
83
|
+
// Pass 2: re-import the tmp using import.sources[0]'s map + format
|
|
84
|
+
// (round-trip requires the bridge's own import map applied to what export wrote).
|
|
85
|
+
const pass2 = [];
|
|
86
|
+
for await (const { record } of parseRecords(descriptor.name, tmpPath, importSource.format)) {
|
|
87
|
+
const mapped = applyMap(importSource.map, record);
|
|
88
|
+
pass2.push(mapped);
|
|
89
|
+
}
|
|
90
|
+
// Match pass1[expected] with pass2 by key (foreignId if present, else content)
|
|
91
|
+
// and diff the stable fields.
|
|
92
|
+
const keyOf = (m) => m.foreignId ?? (m.content ? `content:${m.content}` : "");
|
|
93
|
+
const pass2ByKey = new Map();
|
|
94
|
+
for (const m of pass2)
|
|
95
|
+
pass2ByKey.set(keyOf(m), m);
|
|
96
|
+
const mismatches = [];
|
|
97
|
+
const missingInPass2 = [];
|
|
98
|
+
for (let i = 0; i < expected.length; i++) {
|
|
99
|
+
const a = expected[i];
|
|
100
|
+
const key = keyOf(a);
|
|
101
|
+
const b = pass2ByKey.get(key);
|
|
102
|
+
if (!b) {
|
|
103
|
+
missingInPass2.push({ ordinal: i + 1, key });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const field of ROUND_TRIP_STABLE_FIELDS) {
|
|
107
|
+
if (!deepEqualField(a[field], b[field])) {
|
|
108
|
+
mismatches.push({ ordinal: i + 1, key, field, expected: a[field], got: b[field] });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Matched this pass2 entry; remove so leftover tracking works
|
|
112
|
+
pass2ByKey.delete(key);
|
|
113
|
+
}
|
|
114
|
+
const unexpectedInPass2 = Array.from(pass2ByKey.keys()).map((key) => ({ key }));
|
|
115
|
+
const passed = mismatches.length === 0 && missingInPass2.length === 0 && unexpectedInPass2.length === 0;
|
|
116
|
+
return {
|
|
117
|
+
passed,
|
|
118
|
+
expectedCount: expected.length,
|
|
119
|
+
actualCount: pass2.length,
|
|
120
|
+
mismatches,
|
|
121
|
+
missingInPass2,
|
|
122
|
+
unexpectedInPass2,
|
|
123
|
+
tmpExportPath: tmpPath,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function deepEqualField(a, b) {
|
|
127
|
+
// tags is the only array-valued stable field today; deep-equal shallowly
|
|
128
|
+
// (order matters per our mapping; if a bridge re-orders tags, that's a
|
|
129
|
+
// round-trip regression worth catching).
|
|
130
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
131
|
+
if (a.length !== b.length)
|
|
132
|
+
return false;
|
|
133
|
+
for (let i = 0; i < a.length; i++)
|
|
134
|
+
if (a[i] !== b[i])
|
|
135
|
+
return false;
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
// string / number / boolean / undefined: strict equality
|
|
139
|
+
return a === b;
|
|
140
|
+
}
|
|
141
|
+
function suffixForFormat(format) {
|
|
142
|
+
switch (format) {
|
|
143
|
+
case "jsonl": return "jsonl";
|
|
144
|
+
case "json": return "json";
|
|
145
|
+
case "yaml": return "yaml";
|
|
146
|
+
case "markdown-frontmatter": return "md";
|
|
147
|
+
default: return "out";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function resolvePath(cwd, p) {
|
|
151
|
+
return isAbsolute(p) ? p : join(cwd, p);
|
|
152
|
+
}
|
|
153
|
+
// Keep randomUUID imported; future slice may use it for named tmp files.
|
|
154
|
+
void randomUUID;
|
package/dist/cli.js
CHANGED
|
@@ -324,6 +324,51 @@ async function seedAgentViaOpsApi(opsPort, agentId, pubKeyB64url, adminUser, adm
|
|
|
324
324
|
throw new Error(`Operations API insert failed (${res.status}): ${text}`);
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
|
+
// ─── Upgrade presence probes ──────────────────────────────────────────────────
|
|
328
|
+
//
|
|
329
|
+
// `flair upgrade` previously called `npm list -g <pkg>` to detect the installed
|
|
330
|
+
// version. That assumed the default npm global prefix and failed (silently,
|
|
331
|
+
// reporting "not installed") for mise / fnm / nvm / volta users whose prefix
|
|
332
|
+
// lives elsewhere — including for the running flair binary itself, which is
|
|
333
|
+
// clearly installed somewhere. These probes locate the package regardless of
|
|
334
|
+
// install path.
|
|
335
|
+
export function probeBinVersion(execFileSync, bin) {
|
|
336
|
+
// Run the binary's --version via argv (no shell). PATH resolution still
|
|
337
|
+
// happens (so we find the binary wherever npm/mise/fnm installed it),
|
|
338
|
+
// but there's no shell-string to inject into. CodeQL-safe and simpler.
|
|
339
|
+
try {
|
|
340
|
+
const out = execFileSync(bin, ["--version"], {
|
|
341
|
+
encoding: "utf-8",
|
|
342
|
+
timeout: 5000,
|
|
343
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
344
|
+
}).trim();
|
|
345
|
+
if (!out)
|
|
346
|
+
return null;
|
|
347
|
+
// Accept either "0.6.0" on its own or a line containing a semver.
|
|
348
|
+
const m = out.match(/\b(\d+\.\d+\.\d+(?:[\d.a-z.-]*)?)\b/);
|
|
349
|
+
return m ? m[1] : null;
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
export function probeLibVersion(pkgName) {
|
|
356
|
+
// Resolve the package's package.json from the running flair's module graph.
|
|
357
|
+
// If the lib is installed anywhere Node can see (including bundled as a
|
|
358
|
+
// dep of flair itself, sibling global install, or linked workspace), this
|
|
359
|
+
// finds it. If it's truly missing, require.resolve throws → null.
|
|
360
|
+
try {
|
|
361
|
+
const { createRequire } = require("node:module");
|
|
362
|
+
const req = createRequire(import.meta.url);
|
|
363
|
+
const pkgJsonPath = req.resolve(`${pkgName}/package.json`);
|
|
364
|
+
const { readFileSync } = require("node:fs");
|
|
365
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
366
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
327
372
|
export function templateSoul(choice) {
|
|
328
373
|
const templates = {
|
|
329
374
|
"1": [
|
|
@@ -2363,52 +2408,80 @@ program
|
|
|
2363
2408
|
.option("--check", "Only check for updates, don't install")
|
|
2364
2409
|
.option("--restart", "Restart Flair after upgrade")
|
|
2365
2410
|
.action(async (opts) => {
|
|
2366
|
-
const { execSync } = await import("node:child_process");
|
|
2411
|
+
const { execSync, execFileSync } = await import("node:child_process");
|
|
2367
2412
|
const checkOnly = opts.check ?? false;
|
|
2368
2413
|
console.log("Checking for updates...\n");
|
|
2414
|
+
// Per-package install probes. `npm list -g` assumed the default global
|
|
2415
|
+
// prefix and silently mis-reported "not installed" for anyone using
|
|
2416
|
+
// mise / fnm / nvm / volta / non-default-prefix npm — including the
|
|
2417
|
+
// running flair binary itself, which was obviously installed. Each
|
|
2418
|
+
// entry now has a locator that works regardless of install path:
|
|
2419
|
+
//
|
|
2420
|
+
// - For packages with a bin: shell out to the bin with --version
|
|
2421
|
+
// (same PATH lookup that got them invokable in the first place).
|
|
2422
|
+
// - For library packages: require.resolve the package.json from the
|
|
2423
|
+
// running flair's module graph (works whether it's a sibling
|
|
2424
|
+
// global install or a bundled dep).
|
|
2369
2425
|
const packages = [
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2426
|
+
{
|
|
2427
|
+
name: "@tpsdev-ai/flair",
|
|
2428
|
+
probe: () => probeBinVersion(execFileSync, "flair"),
|
|
2429
|
+
},
|
|
2430
|
+
{
|
|
2431
|
+
name: "@tpsdev-ai/flair-client",
|
|
2432
|
+
probe: () => probeLibVersion("@tpsdev-ai/flair-client"),
|
|
2433
|
+
},
|
|
2434
|
+
{
|
|
2435
|
+
name: "@tpsdev-ai/flair-mcp",
|
|
2436
|
+
probe: () => probeBinVersion(execFileSync, "flair-mcp"),
|
|
2437
|
+
},
|
|
2373
2438
|
];
|
|
2374
|
-
const
|
|
2375
|
-
for (const
|
|
2439
|
+
const findings = [];
|
|
2440
|
+
for (const { name, probe } of packages) {
|
|
2376
2441
|
try {
|
|
2377
|
-
const res = await fetch(`https://registry.npmjs.org/${
|
|
2442
|
+
const res = await fetch(`https://registry.npmjs.org/${name}/latest`, { signal: AbortSignal.timeout(5000) });
|
|
2378
2443
|
if (!res.ok)
|
|
2379
2444
|
continue;
|
|
2380
2445
|
const data = await res.json();
|
|
2381
2446
|
const latest = data.version ?? "unknown";
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
const upToDate = installed === latest;
|
|
2390
|
-
const icon = upToDate ? "✅" : "⬆️";
|
|
2391
|
-
console.log(` ${icon} ${pkg}: ${installed} → ${latest}${upToDate ? " (current)" : ""}`);
|
|
2392
|
-
if (!upToDate && installed !== "not installed") {
|
|
2393
|
-
upgrades.push({ pkg, installed, latest });
|
|
2394
|
-
}
|
|
2447
|
+
const installed = probe();
|
|
2448
|
+
const status = installed === null ? "missing" : installed === latest ? "current" : "outdated";
|
|
2449
|
+
findings.push({ name, installed, latest, status });
|
|
2450
|
+
const icon = status === "current" ? "✅" : status === "outdated" ? "⬆️" : "❔";
|
|
2451
|
+
const installedLabel = installed ?? "not detected";
|
|
2452
|
+
const suffix = status === "current" ? " (current)" : status === "missing" ? " (run: npm install -g)" : "";
|
|
2453
|
+
console.log(` ${icon} ${name}: ${installedLabel} → ${latest}${suffix}`);
|
|
2395
2454
|
}
|
|
2396
2455
|
catch { /* skip unavailable packages */ }
|
|
2397
2456
|
}
|
|
2398
|
-
|
|
2457
|
+
const outdated = findings.filter((f) => f.status === "outdated");
|
|
2458
|
+
const missing = findings.filter((f) => f.status === "missing");
|
|
2459
|
+
const upgrades = outdated.map(({ name, installed, latest }) => ({ pkg: name, installed: installed ?? "unknown", latest }));
|
|
2460
|
+
if (outdated.length === 0 && missing.length === 0) {
|
|
2399
2461
|
console.log("\n✅ Everything is up to date.");
|
|
2400
2462
|
return;
|
|
2401
2463
|
}
|
|
2464
|
+
if (missing.length > 0 && outdated.length === 0) {
|
|
2465
|
+
console.log(`\n❔ ${missing.length} package${missing.length > 1 ? "s" : ""} not detected — all detected packages are up to date.`);
|
|
2466
|
+
console.log(` Install missing: npm install -g ${missing.map((f) => f.name).join(" ")}`);
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2402
2469
|
if (checkOnly) {
|
|
2403
|
-
console.log(`\n${
|
|
2470
|
+
console.log(`\n${outdated.length} update${outdated.length > 1 ? "s" : ""} available. Run: flair upgrade`);
|
|
2471
|
+
if (missing.length > 0) {
|
|
2472
|
+
console.log(`${missing.length} package${missing.length > 1 ? "s" : ""} not detected${missing.length > 0 ? ": " + missing.map((f) => f.name).join(", ") : ""}.`);
|
|
2473
|
+
}
|
|
2404
2474
|
return;
|
|
2405
2475
|
}
|
|
2406
|
-
// Perform upgrade
|
|
2476
|
+
// Perform upgrade. `latest` comes from the npm registry's HTTP
|
|
2477
|
+
// response, so CodeQL (correctly) treats it as untrusted input.
|
|
2478
|
+
// Use execFileSync with argv — the spec `<name>@<version>` becomes a
|
|
2479
|
+
// single argument to npm, no shell to inject into.
|
|
2407
2480
|
console.log(`\nUpgrading ${upgrades.length} package${upgrades.length > 1 ? "s" : ""}...\n`);
|
|
2408
2481
|
for (const { pkg, latest } of upgrades) {
|
|
2409
2482
|
try {
|
|
2410
2483
|
console.log(` Installing ${pkg}@${latest}...`);
|
|
2411
|
-
|
|
2484
|
+
execFileSync("npm", ["install", "-g", `${pkg}@${latest}`], { stdio: "pipe" });
|
|
2412
2485
|
console.log(` ✅ ${pkg}@${latest} installed`);
|
|
2413
2486
|
}
|
|
2414
2487
|
catch (err) {
|
|
@@ -3679,15 +3752,78 @@ bridge
|
|
|
3679
3752
|
process.exit(1);
|
|
3680
3753
|
}
|
|
3681
3754
|
});
|
|
3682
|
-
// `test` still stubbed — round-trip harness lands in slice 3b.
|
|
3683
3755
|
bridge
|
|
3684
|
-
.command("test <name>
|
|
3685
|
-
.description("
|
|
3686
|
-
.
|
|
3687
|
-
.
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
process.
|
|
3756
|
+
.command("test <name>")
|
|
3757
|
+
.description("Round-trip a bridge through its fixture: import → export → re-import → diff. Pass iff the stable fields (content/subject/tags/durability) match.")
|
|
3758
|
+
.option("--fixture <path>", "Override the import source path (defaults to descriptor's import.sources[0].path)")
|
|
3759
|
+
.option("--cwd <dir>", "Filesystem root the descriptor's relative paths resolve against (default: cwd)")
|
|
3760
|
+
.option("--json", "Emit the full RoundTripResult as JSON on stdout")
|
|
3761
|
+
.action(async (name, opts) => {
|
|
3762
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
3763
|
+
const { discover } = await import("./bridges/discover.js");
|
|
3764
|
+
const { builtinDiscoveryRecords } = await import("./bridges/builtins/index.js");
|
|
3765
|
+
const { loadDescriptor } = await import("./bridges/runtime/load-descriptor.js");
|
|
3766
|
+
const { runRoundTrip } = await import("./bridges/runtime/roundtrip.js");
|
|
3767
|
+
const { BridgeRuntimeError } = await import("./bridges/types.js");
|
|
3768
|
+
const found = await discover({ builtins: builtinDiscoveryRecords() });
|
|
3769
|
+
const target = found.find((b) => b.name === name);
|
|
3770
|
+
if (!target) {
|
|
3771
|
+
console.error(`No bridge named "${name}" — run \`flair bridge list\` to see installed bridges.`);
|
|
3772
|
+
process.exit(1);
|
|
3773
|
+
}
|
|
3774
|
+
let descriptor;
|
|
3775
|
+
try {
|
|
3776
|
+
descriptor = await loadDescriptor(target);
|
|
3777
|
+
}
|
|
3778
|
+
catch (err) {
|
|
3779
|
+
printBridgeError(err);
|
|
3780
|
+
process.exit(1);
|
|
3781
|
+
}
|
|
3782
|
+
try {
|
|
3783
|
+
const result = await runRoundTrip({ descriptor, cwd, fixturePath: opts.fixture });
|
|
3784
|
+
if (opts.json) {
|
|
3785
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3786
|
+
process.exit(result.passed ? 0 : 1);
|
|
3787
|
+
}
|
|
3788
|
+
if (result.passed) {
|
|
3789
|
+
console.log(`✅ ${descriptor.name} round-trip passed (${result.expectedCount} record${result.expectedCount === 1 ? "" : "s"}).`);
|
|
3790
|
+
process.exit(0);
|
|
3791
|
+
}
|
|
3792
|
+
console.log(`❌ ${descriptor.name} round-trip failed.`);
|
|
3793
|
+
console.log(` expected ${result.expectedCount} records, got ${result.actualCount} back.`);
|
|
3794
|
+
if (result.missingInPass2.length > 0) {
|
|
3795
|
+
console.log(` missing from re-import (${result.missingInPass2.length}):`);
|
|
3796
|
+
for (const m of result.missingInPass2.slice(0, 5))
|
|
3797
|
+
console.log(` - ${m.key}`);
|
|
3798
|
+
if (result.missingInPass2.length > 5)
|
|
3799
|
+
console.log(` ... ${result.missingInPass2.length - 5} more`);
|
|
3800
|
+
}
|
|
3801
|
+
if (result.unexpectedInPass2.length > 0) {
|
|
3802
|
+
console.log(` unexpected extras in re-import (${result.unexpectedInPass2.length}):`);
|
|
3803
|
+
for (const m of result.unexpectedInPass2.slice(0, 5))
|
|
3804
|
+
console.log(` - ${m.key}`);
|
|
3805
|
+
if (result.unexpectedInPass2.length > 5)
|
|
3806
|
+
console.log(` ... ${result.unexpectedInPass2.length - 5} more`);
|
|
3807
|
+
}
|
|
3808
|
+
if (result.mismatches.length > 0) {
|
|
3809
|
+
console.log(` field mismatches (${result.mismatches.length}):`);
|
|
3810
|
+
for (const m of result.mismatches.slice(0, 10)) {
|
|
3811
|
+
console.log(` - record ${m.ordinal} (${m.key}) field ${m.field}: expected ${JSON.stringify(m.expected)}, got ${JSON.stringify(m.got)}`);
|
|
3812
|
+
}
|
|
3813
|
+
if (result.mismatches.length > 10)
|
|
3814
|
+
console.log(` ... ${result.mismatches.length - 10} more`);
|
|
3815
|
+
}
|
|
3816
|
+
console.log(`\n Intermediate export at: ${result.tmpExportPath}`);
|
|
3817
|
+
process.exit(1);
|
|
3818
|
+
}
|
|
3819
|
+
catch (err) {
|
|
3820
|
+
if (err instanceof BridgeRuntimeError) {
|
|
3821
|
+
printBridgeError(err);
|
|
3822
|
+
process.exit(1);
|
|
3823
|
+
}
|
|
3824
|
+
console.error(`Bridge test failed: ${err?.message ?? err}`);
|
|
3825
|
+
process.exit(1);
|
|
3826
|
+
}
|
|
3691
3827
|
});
|
|
3692
3828
|
function printBridgeError(err) {
|
|
3693
3829
|
// Pretty-print BridgeRuntimeError as the structured shape from §10 of the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|