@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/index.d.cts CHANGED
@@ -313,7 +313,7 @@ declare function pack(bundle: KbRecord[], rootId: string, options?: KbPackOption
313
313
 
314
314
  declare const LOG_FILE = "log.jsonl";
315
315
  declare const kbLogEntrySchema: z.ZodObject<{
316
- at: z.ZodString;
316
+ at: z.ZodISODateTime;
317
317
  by: z.ZodString;
318
318
  operation: z.ZodString;
319
319
  conceptId: z.ZodString;
@@ -569,6 +569,47 @@ declare class KbStore {
569
569
  * otherwise try to read it mid-write.
570
570
  */
571
571
  private publish;
572
+ /**
573
+ * Declares union merge for the log, so two worktrees writing the same
574
+ * bundle interleave their `log.jsonl` lines on merge rather than one
575
+ * side's appends silently losing to git's ordinary line-level merge.
576
+ *
577
+ * Called from `record` — every path that appends a log line, not just
578
+ * `write` — so a bundle only ever mutated through `setStatus`/`verify`/
579
+ * `supersede` still gets it. There is no cheaper reliable signal for
580
+ * "first write" than checking the file itself, and after the first call
581
+ * the check is a no-op `readFile`.
582
+ *
583
+ * A missing `.gitattributes` is created outright, with `wx` (exclusive
584
+ * create) rather than a plain write: if another process's `write()` won a
585
+ * race and created the file between the `readFile` below and this call,
586
+ * `wx` fails instead of truncating what that writer just wrote, and the
587
+ * failure is swallowed by the catch below same as any other best-effort
588
+ * miss. A file that exists but declares no merge strategy for the log
589
+ * gets the line appended, never a wholesale rewrite; one that already
590
+ * declares any merge strategy — this one or a user's own — is left alone
591
+ * entirely (see `hasMergeDeclaration`).
592
+ *
593
+ * `readFile` failing is `existing === null` only for `ENOENT` — genuinely
594
+ * missing. Any other error (a permission problem, a transient `EMFILE`,
595
+ * the path being a directory) is *not* "missing" and must not fall into
596
+ * the create branch, which would truncate whatever is actually there with
597
+ * just the union-merge line: that is the file-destroying bug this
598
+ * function exists to avoid, not commit. An unreadable existing file is
599
+ * therefore left untouched and reported as a failure like any other.
600
+ *
601
+ * Two processes racing the append branch — both read a file without the
602
+ * line, both append it — is possible and left unguarded: `appendFile` is
603
+ * `O_APPEND`, so the result is two copies of the same line rather than a
604
+ * torn write, and `hasMergeDeclaration` sees a duplicate declaration as
605
+ * "already declared" on the next call. A cheap-to-detect, harmless-to-
606
+ * leave residue, not a reason to add a cross-process lock (see
607
+ * `ARCHITECTURE.md`'s rejection of one for the same trade on records).
608
+ *
609
+ * Best-effort, like the log append it precedes: failing to write this
610
+ * file must not fail the mutation it guards.
611
+ */
612
+ private ensureGitattributes;
572
613
  /** Appends one log line. Failing to log must not fail the mutation. */
573
614
  private record;
574
615
  private parse;
package/dist/index.d.ts CHANGED
@@ -313,7 +313,7 @@ declare function pack(bundle: KbRecord[], rootId: string, options?: KbPackOption
313
313
 
314
314
  declare const LOG_FILE = "log.jsonl";
315
315
  declare const kbLogEntrySchema: z.ZodObject<{
316
- at: z.ZodString;
316
+ at: z.ZodISODateTime;
317
317
  by: z.ZodString;
318
318
  operation: z.ZodString;
319
319
  conceptId: z.ZodString;
@@ -569,6 +569,47 @@ declare class KbStore {
569
569
  * otherwise try to read it mid-write.
570
570
  */
571
571
  private publish;
572
+ /**
573
+ * Declares union merge for the log, so two worktrees writing the same
574
+ * bundle interleave their `log.jsonl` lines on merge rather than one
575
+ * side's appends silently losing to git's ordinary line-level merge.
576
+ *
577
+ * Called from `record` — every path that appends a log line, not just
578
+ * `write` — so a bundle only ever mutated through `setStatus`/`verify`/
579
+ * `supersede` still gets it. There is no cheaper reliable signal for
580
+ * "first write" than checking the file itself, and after the first call
581
+ * the check is a no-op `readFile`.
582
+ *
583
+ * A missing `.gitattributes` is created outright, with `wx` (exclusive
584
+ * create) rather than a plain write: if another process's `write()` won a
585
+ * race and created the file between the `readFile` below and this call,
586
+ * `wx` fails instead of truncating what that writer just wrote, and the
587
+ * failure is swallowed by the catch below same as any other best-effort
588
+ * miss. A file that exists but declares no merge strategy for the log
589
+ * gets the line appended, never a wholesale rewrite; one that already
590
+ * declares any merge strategy — this one or a user's own — is left alone
591
+ * entirely (see `hasMergeDeclaration`).
592
+ *
593
+ * `readFile` failing is `existing === null` only for `ENOENT` — genuinely
594
+ * missing. Any other error (a permission problem, a transient `EMFILE`,
595
+ * the path being a directory) is *not* "missing" and must not fall into
596
+ * the create branch, which would truncate whatever is actually there with
597
+ * just the union-merge line: that is the file-destroying bug this
598
+ * function exists to avoid, not commit. An unreadable existing file is
599
+ * therefore left untouched and reported as a failure like any other.
600
+ *
601
+ * Two processes racing the append branch — both read a file without the
602
+ * line, both append it — is possible and left unguarded: `appendFile` is
603
+ * `O_APPEND`, so the result is two copies of the same line rather than a
604
+ * torn write, and `hasMergeDeclaration` sees a duplicate declaration as
605
+ * "already declared" on the next call. A cheap-to-detect, harmless-to-
606
+ * leave residue, not a reason to add a cross-process lock (see
607
+ * `ARCHITECTURE.md`'s rejection of one for the same trade on records).
608
+ *
609
+ * Best-effort, like the log append it precedes: failing to write this
610
+ * file must not fail the mutation it guards.
611
+ */
612
+ private ensureGitattributes;
572
613
  /** Appends one log line. Failing to log must not fail the mutation. */
573
614
  private record;
574
615
  private parse;
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  runKbCli
3
- } from "./chunk-MGPYUZOM.js";
3
+ } from "./chunk-KVEEISYQ.js";
4
4
  import {
5
5
  createKbMcpServer,
6
6
  runKbMcpServer
7
- } from "./chunk-YYX6CX6V.js";
7
+ } from "./chunk-MWWDD23L.js";
8
8
  import {
9
9
  BaseError,
10
10
  CONTEXT_BEGIN,
@@ -95,7 +95,7 @@ import {
95
95
  trace,
96
96
  unpinBase,
97
97
  validateBundle
98
- } from "./chunk-YJK7KGHN.js";
98
+ } from "./chunk-OFDWRMY6.js";
99
99
 
100
100
  // src/match-diff.ts
101
101
  function matchToDiff(files, records, options = {}) {
package/dist/mcp-main.cjs CHANGED
@@ -1813,7 +1813,15 @@ var import_zod19 = require("zod");
1813
1813
  var import_zod18 = require("zod");
1814
1814
  var LOG_FILE = "log.jsonl";
1815
1815
  var kbLogEntrySchema = import_zod18.z.object({
1816
- at: import_zod18.z.string().min(1),
1816
+ // Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
1817
+ // below), and a value that isn't actually chronological — a Unix
1818
+ // timestamp, a human-typed date, garbage — would sort wrong without
1819
+ // ever failing to parse. `z.iso.datetime()` accepts exactly what
1820
+ // `record()` writes (`Date#toISOString()`: full precision, `Z` offset)
1821
+ // and rejects everything else, including a non-`Z` offset — so a
1822
+ // malformed `at` is reported the same way a malformed line already is,
1823
+ // rather than silently sorting into the wrong place.
1824
+ at: import_zod18.z.iso.datetime(),
1817
1825
  by: import_zod18.z.string().min(1),
1818
1826
  operation: import_zod18.z.string().min(1),
1819
1827
  conceptId: import_zod18.z.string().min(1),
@@ -1827,6 +1835,7 @@ function renderLogEntry(entry) {
1827
1835
  function parseLog(raw) {
1828
1836
  const entries = [];
1829
1837
  const malformed = [];
1838
+ const seen = /* @__PURE__ */ new Set();
1830
1839
  raw.split("\n").forEach((text, index) => {
1831
1840
  if (!text.trim()) return;
1832
1841
  let value;
@@ -1841,8 +1850,14 @@ function parseLog(raw) {
1841
1850
  malformed.push({ line: index + 1, text });
1842
1851
  return;
1843
1852
  }
1853
+ const key = JSON.stringify(parsed.data);
1854
+ if (seen.has(key)) return;
1855
+ seen.add(key);
1844
1856
  entries.push(parsed.data);
1845
1857
  });
1858
+ entries.sort(
1859
+ (left, right) => left.at < right.at ? -1 : left.at > right.at ? 1 : 0
1860
+ );
1846
1861
  return { entries, malformed };
1847
1862
  }
1848
1863
 
@@ -2509,6 +2524,30 @@ function typeRank(record) {
2509
2524
  return index === -1 ? TYPE_PRIORITY.length : index;
2510
2525
  }
2511
2526
 
2527
+ // src/kb-gitattributes.ts
2528
+ var GITATTRIBUTES_FILE = ".gitattributes";
2529
+ var UNION_MERGE_LINE = `${LOG_FILE} text eol=lf merge=union`;
2530
+ function parseLine(line) {
2531
+ const trimmed = line.trim();
2532
+ if (!trimmed || trimmed.startsWith("#")) return null;
2533
+ const [pattern, ...attrs] = trimmed.split(/\s+/);
2534
+ return pattern === void 0 ? null : { pattern, attrs };
2535
+ }
2536
+ function hasMergeDeclaration(contents) {
2537
+ return contents.split("\n").some((line) => {
2538
+ const parsed = parseLine(line);
2539
+ if (!parsed || parsed.pattern !== LOG_FILE) return false;
2540
+ return parsed.attrs.some(
2541
+ (attr) => attr === "merge" || attr === "-merge" || attr.startsWith("merge=")
2542
+ );
2543
+ });
2544
+ }
2545
+ function appendUnionMergeLine(contents) {
2546
+ const separator = contents.length === 0 || contents.endsWith("\n") ? "" : "\n";
2547
+ return `${separator}${UNION_MERGE_LINE}
2548
+ `;
2549
+ }
2550
+
2512
2551
  // src/kb-store.ts
2513
2552
  var KB_DIR = (0, import_node_path6.join)(".strauss", "kb");
2514
2553
  var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
@@ -2934,8 +2973,87 @@ ${answer}
2934
2973
  await (0, import_promises4.unlink)(staging).catch(() => void 0);
2935
2974
  }
2936
2975
  }
2976
+ /**
2977
+ * Declares union merge for the log, so two worktrees writing the same
2978
+ * bundle interleave their `log.jsonl` lines on merge rather than one
2979
+ * side's appends silently losing to git's ordinary line-level merge.
2980
+ *
2981
+ * Called from `record` — every path that appends a log line, not just
2982
+ * `write` — so a bundle only ever mutated through `setStatus`/`verify`/
2983
+ * `supersede` still gets it. There is no cheaper reliable signal for
2984
+ * "first write" than checking the file itself, and after the first call
2985
+ * the check is a no-op `readFile`.
2986
+ *
2987
+ * A missing `.gitattributes` is created outright, with `wx` (exclusive
2988
+ * create) rather than a plain write: if another process's `write()` won a
2989
+ * race and created the file between the `readFile` below and this call,
2990
+ * `wx` fails instead of truncating what that writer just wrote, and the
2991
+ * failure is swallowed by the catch below same as any other best-effort
2992
+ * miss. A file that exists but declares no merge strategy for the log
2993
+ * gets the line appended, never a wholesale rewrite; one that already
2994
+ * declares any merge strategy — this one or a user's own — is left alone
2995
+ * entirely (see `hasMergeDeclaration`).
2996
+ *
2997
+ * `readFile` failing is `existing === null` only for `ENOENT` — genuinely
2998
+ * missing. Any other error (a permission problem, a transient `EMFILE`,
2999
+ * the path being a directory) is *not* "missing" and must not fall into
3000
+ * the create branch, which would truncate whatever is actually there with
3001
+ * just the union-merge line: that is the file-destroying bug this
3002
+ * function exists to avoid, not commit. An unreadable existing file is
3003
+ * therefore left untouched and reported as a failure like any other.
3004
+ *
3005
+ * Two processes racing the append branch — both read a file without the
3006
+ * line, both append it — is possible and left unguarded: `appendFile` is
3007
+ * `O_APPEND`, so the result is two copies of the same line rather than a
3008
+ * torn write, and `hasMergeDeclaration` sees a duplicate declaration as
3009
+ * "already declared" on the next call. A cheap-to-detect, harmless-to-
3010
+ * leave residue, not a reason to add a cross-process lock (see
3011
+ * `ARCHITECTURE.md`'s rejection of one for the same trade on records).
3012
+ *
3013
+ * Best-effort, like the log append it precedes: failing to write this
3014
+ * file must not fail the mutation it guards.
3015
+ */
3016
+ async ensureGitattributes(root) {
3017
+ const target = (0, import_node_path6.join)(root, GITATTRIBUTES_FILE);
3018
+ try {
3019
+ let existing;
3020
+ try {
3021
+ existing = await (0, import_promises4.readFile)(target, "utf8");
3022
+ } catch (error) {
3023
+ if (error.code !== "ENOENT") throw error;
3024
+ existing = null;
3025
+ }
3026
+ if (existing === null) {
3027
+ await (0, import_promises4.writeFile)(target, appendUnionMergeLine(""), {
3028
+ encoding: "utf8",
3029
+ flag: "wx"
3030
+ });
3031
+ this.logger.info?.({
3032
+ operation: "kb.gitattributes.ensure",
3033
+ bundlePath: root,
3034
+ outcome: "created"
3035
+ });
3036
+ return;
3037
+ }
3038
+ if (!hasMergeDeclaration(existing)) {
3039
+ await (0, import_promises4.appendFile)(target, appendUnionMergeLine(existing), "utf8");
3040
+ this.logger.info?.({
3041
+ operation: "kb.gitattributes.ensure",
3042
+ bundlePath: root,
3043
+ outcome: "appended"
3044
+ });
3045
+ }
3046
+ } catch (error) {
3047
+ this.logger.warn?.({
3048
+ operation: "kb.gitattributes.ensure",
3049
+ outcome: "failed",
3050
+ error: error instanceof Error ? error.message : "unknown"
3051
+ });
3052
+ }
3053
+ }
2937
3054
  /** Appends one log line. Failing to log must not fail the mutation. */
2938
3055
  async record(root, entry) {
3056
+ await this.ensureGitattributes(root);
2939
3057
  const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
2940
3058
  await (0, import_promises4.appendFile)((0, import_node_path6.join)(root, LOG_FILE), line, "utf8").catch((error) => {
2941
3059
  this.logger.warn?.({
@@ -3009,7 +3127,7 @@ function digest(contents) {
3009
3127
  }
3010
3128
 
3011
3129
  // src/version.ts
3012
- var VERSION = true ? "0.1.8" : "0.0.0-dev";
3130
+ var VERSION = true ? "0.1.9" : "0.0.0-dev";
3013
3131
 
3014
3132
  // src/mcp.ts
3015
3133
  function createKbMcpServer() {