@saasontools/strauss-kb 0.1.8 → 0.1.9

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/cli-main.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runKbCli
4
- } from "./chunk-MGPYUZOM.js";
5
- import "./chunk-YJK7KGHN.js";
4
+ } from "./chunk-KVEEISYQ.js";
5
+ import "./chunk-OFDWRMY6.js";
6
6
 
7
7
  // src/cli-main.ts
8
8
  runKbCli(process.argv.slice(2)).catch((error) => {
package/dist/index.cjs CHANGED
@@ -503,7 +503,15 @@ var import_node_path = require("path");
503
503
  var import_zod2 = require("zod");
504
504
  var LOG_FILE = "log.jsonl";
505
505
  var kbLogEntrySchema = import_zod2.z.object({
506
- at: import_zod2.z.string().min(1),
506
+ // Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
507
+ // below), and a value that isn't actually chronological — a Unix
508
+ // timestamp, a human-typed date, garbage — would sort wrong without
509
+ // ever failing to parse. `z.iso.datetime()` accepts exactly what
510
+ // `record()` writes (`Date#toISOString()`: full precision, `Z` offset)
511
+ // and rejects everything else, including a non-`Z` offset — so a
512
+ // malformed `at` is reported the same way a malformed line already is,
513
+ // rather than silently sorting into the wrong place.
514
+ at: import_zod2.z.iso.datetime(),
507
515
  by: import_zod2.z.string().min(1),
508
516
  operation: import_zod2.z.string().min(1),
509
517
  conceptId: import_zod2.z.string().min(1),
@@ -517,6 +525,7 @@ function renderLogEntry(entry) {
517
525
  function parseLog(raw) {
518
526
  const entries = [];
519
527
  const malformed = [];
528
+ const seen = /* @__PURE__ */ new Set();
520
529
  raw.split("\n").forEach((text, index) => {
521
530
  if (!text.trim()) return;
522
531
  let value;
@@ -531,8 +540,14 @@ function parseLog(raw) {
531
540
  malformed.push({ line: index + 1, text });
532
541
  return;
533
542
  }
543
+ const key2 = JSON.stringify(parsed.data);
544
+ if (seen.has(key2)) return;
545
+ seen.add(key2);
534
546
  entries.push(parsed.data);
535
547
  });
548
+ entries.sort(
549
+ (left, right) => left.at < right.at ? -1 : left.at > right.at ? 1 : 0
550
+ );
536
551
  return { entries, malformed };
537
552
  }
538
553
 
@@ -818,6 +833,30 @@ function typeRank(record) {
818
833
  return index === -1 ? TYPE_PRIORITY.length : index;
819
834
  }
820
835
 
836
+ // src/kb-gitattributes.ts
837
+ var GITATTRIBUTES_FILE = ".gitattributes";
838
+ var UNION_MERGE_LINE = `${LOG_FILE} text eol=lf merge=union`;
839
+ function parseLine(line) {
840
+ const trimmed = line.trim();
841
+ if (!trimmed || trimmed.startsWith("#")) return null;
842
+ const [pattern, ...attrs] = trimmed.split(/\s+/);
843
+ return pattern === void 0 ? null : { pattern, attrs };
844
+ }
845
+ function hasMergeDeclaration(contents) {
846
+ return contents.split("\n").some((line) => {
847
+ const parsed = parseLine(line);
848
+ if (!parsed || parsed.pattern !== LOG_FILE) return false;
849
+ return parsed.attrs.some(
850
+ (attr) => attr === "merge" || attr === "-merge" || attr.startsWith("merge=")
851
+ );
852
+ });
853
+ }
854
+ function appendUnionMergeLine(contents) {
855
+ const separator = contents.length === 0 || contents.endsWith("\n") ? "" : "\n";
856
+ return `${separator}${UNION_MERGE_LINE}
857
+ `;
858
+ }
859
+
821
860
  // src/kb-store.ts
822
861
  var KB_DIR = (0, import_node_path2.join)(".strauss", "kb");
823
862
  var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
@@ -1243,8 +1282,87 @@ ${answer}
1243
1282
  await (0, import_promises2.unlink)(staging).catch(() => void 0);
1244
1283
  }
1245
1284
  }
1285
+ /**
1286
+ * Declares union merge for the log, so two worktrees writing the same
1287
+ * bundle interleave their `log.jsonl` lines on merge rather than one
1288
+ * side's appends silently losing to git's ordinary line-level merge.
1289
+ *
1290
+ * Called from `record` — every path that appends a log line, not just
1291
+ * `write` — so a bundle only ever mutated through `setStatus`/`verify`/
1292
+ * `supersede` still gets it. There is no cheaper reliable signal for
1293
+ * "first write" than checking the file itself, and after the first call
1294
+ * the check is a no-op `readFile`.
1295
+ *
1296
+ * A missing `.gitattributes` is created outright, with `wx` (exclusive
1297
+ * create) rather than a plain write: if another process's `write()` won a
1298
+ * race and created the file between the `readFile` below and this call,
1299
+ * `wx` fails instead of truncating what that writer just wrote, and the
1300
+ * failure is swallowed by the catch below same as any other best-effort
1301
+ * miss. A file that exists but declares no merge strategy for the log
1302
+ * gets the line appended, never a wholesale rewrite; one that already
1303
+ * declares any merge strategy — this one or a user's own — is left alone
1304
+ * entirely (see `hasMergeDeclaration`).
1305
+ *
1306
+ * `readFile` failing is `existing === null` only for `ENOENT` — genuinely
1307
+ * missing. Any other error (a permission problem, a transient `EMFILE`,
1308
+ * the path being a directory) is *not* "missing" and must not fall into
1309
+ * the create branch, which would truncate whatever is actually there with
1310
+ * just the union-merge line: that is the file-destroying bug this
1311
+ * function exists to avoid, not commit. An unreadable existing file is
1312
+ * therefore left untouched and reported as a failure like any other.
1313
+ *
1314
+ * Two processes racing the append branch — both read a file without the
1315
+ * line, both append it — is possible and left unguarded: `appendFile` is
1316
+ * `O_APPEND`, so the result is two copies of the same line rather than a
1317
+ * torn write, and `hasMergeDeclaration` sees a duplicate declaration as
1318
+ * "already declared" on the next call. A cheap-to-detect, harmless-to-
1319
+ * leave residue, not a reason to add a cross-process lock (see
1320
+ * `ARCHITECTURE.md`'s rejection of one for the same trade on records).
1321
+ *
1322
+ * Best-effort, like the log append it precedes: failing to write this
1323
+ * file must not fail the mutation it guards.
1324
+ */
1325
+ async ensureGitattributes(root) {
1326
+ const target = (0, import_node_path2.join)(root, GITATTRIBUTES_FILE);
1327
+ try {
1328
+ let existing;
1329
+ try {
1330
+ existing = await (0, import_promises2.readFile)(target, "utf8");
1331
+ } catch (error) {
1332
+ if (error.code !== "ENOENT") throw error;
1333
+ existing = null;
1334
+ }
1335
+ if (existing === null) {
1336
+ await (0, import_promises2.writeFile)(target, appendUnionMergeLine(""), {
1337
+ encoding: "utf8",
1338
+ flag: "wx"
1339
+ });
1340
+ this.logger.info?.({
1341
+ operation: "kb.gitattributes.ensure",
1342
+ bundlePath: root,
1343
+ outcome: "created"
1344
+ });
1345
+ return;
1346
+ }
1347
+ if (!hasMergeDeclaration(existing)) {
1348
+ await (0, import_promises2.appendFile)(target, appendUnionMergeLine(existing), "utf8");
1349
+ this.logger.info?.({
1350
+ operation: "kb.gitattributes.ensure",
1351
+ bundlePath: root,
1352
+ outcome: "appended"
1353
+ });
1354
+ }
1355
+ } catch (error) {
1356
+ this.logger.warn?.({
1357
+ operation: "kb.gitattributes.ensure",
1358
+ outcome: "failed",
1359
+ error: error instanceof Error ? error.message : "unknown"
1360
+ });
1361
+ }
1362
+ }
1246
1363
  /** Appends one log line. Failing to log must not fail the mutation. */
1247
1364
  async record(root, entry) {
1365
+ await this.ensureGitattributes(root);
1248
1366
  const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
1249
1367
  await (0, import_promises2.appendFile)((0, import_node_path2.join)(root, LOG_FILE), line, "utf8").catch((error) => {
1250
1368
  this.logger.warn?.({
@@ -3210,7 +3328,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
3210
3328
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
3211
3329
 
3212
3330
  // src/version.ts
3213
- var VERSION = true ? "0.1.8" : "0.0.0-dev";
3331
+ var VERSION = true ? "0.1.9" : "0.0.0-dev";
3214
3332
 
3215
3333
  // src/mcp.ts
3216
3334
  function createKbMcpServer() {