@saasontools/strauss-kb 0.1.2 → 0.1.3

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.ts CHANGED
@@ -583,6 +583,7 @@ declare const composeInputSchema: z.ZodObject<{
583
583
  last_modified: z.ZodOptional<z.ZodString>;
584
584
  }, z.core.$loose>>>;
585
585
  assumption: z.ZodOptional<z.ZodBoolean>;
586
+ stale_after: z.ZodOptional<z.ZodString>;
586
587
  verify: z.ZodOptional<z.ZodArray<z.ZodString>>;
587
588
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
588
589
  relatedConceptIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -631,9 +632,260 @@ declare const INDEX_FILE = "INDEX.md";
631
632
  * decide what is worth opening, and a list of titles does not answer that.
632
633
  */
633
634
  declare function renderIndex(records: KbRecord[]): string;
635
+ /**
636
+ * One record's index line. The single writer of this shape — `context` emits
637
+ * the same line rather than growing a second index renderer that would drift
638
+ * from this one.
639
+ */
640
+ declare function renderIndexLine(record: KbRecord): string;
634
641
  /** Whether the stored projection still matches the records it claims to index. */
635
642
  declare function indexIsStale(stored: string | null, expected: string): boolean;
636
643
 
644
+ /**
645
+ * Pin manifests, in three layers.
646
+ *
647
+ * Pins are workspace state, not base state: they record which bases a session
648
+ * should be shown at every context birth. The pinned base is never touched —
649
+ * not even its log — because a base must remain copyable without knowing who
650
+ * pins it.
651
+ *
652
+ * The layers, nearest wins when the same base appears in more than one:
653
+ *
654
+ * | layer | file | for |
655
+ * | ------- | --------------------------------------- | -------------------------- |
656
+ * | project | <workspace>/.strauss/kb-pins.json | committed, the team's pins |
657
+ * | local | <workspace>/.strauss/kb-pins.local.json | personal, gitignored |
658
+ * | user | ~/.strauss/kb-pins.json | personal, every workspace |
659
+ *
660
+ * Every manifest's paths resolve against its own root — the workspace for
661
+ * project and local, the home directory for user — so each file is portable
662
+ * with the tree it belongs to. `STRAUSS_KB_USER_ROOT` overrides the user root
663
+ * (tests, unusual homes).
664
+ */
665
+ declare const PINS_FILE: string;
666
+ declare const PINS_LOCAL_FILE: string;
667
+ declare const PIN_LAYERS: readonly ["project", "local", "user"];
668
+ type KbPinLayer = (typeof PIN_LAYERS)[number];
669
+ /**
670
+ * Primary state — it records an intent nothing else holds — but trivially
671
+ * rewritable, so a full rewrite on change is fine and no append log is needed.
672
+ * Unknown keys are preserved on rewrite, the same tolerance the record reader
673
+ * extends to frontmatter it did not write.
674
+ */
675
+ declare const pinSchema: z.ZodObject<{
676
+ path: z.ZodString;
677
+ pinnedAt: z.ZodOptional<z.ZodString>;
678
+ mode: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
679
+ full: "full";
680
+ index: "index";
681
+ }>>>;
682
+ profiles: z.ZodCatch<z.ZodOptional<z.ZodArray<z.ZodString>>>;
683
+ frozen: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
684
+ }, z.core.$loose>;
685
+ declare const pinsManifestSchema: z.ZodObject<{
686
+ pins: z.ZodDefault<z.ZodArray<z.ZodObject<{
687
+ path: z.ZodString;
688
+ pinnedAt: z.ZodOptional<z.ZodString>;
689
+ mode: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
690
+ full: "full";
691
+ index: "index";
692
+ }>>>;
693
+ profiles: z.ZodCatch<z.ZodOptional<z.ZodArray<z.ZodString>>>;
694
+ frozen: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
695
+ }, z.core.$loose>>>;
696
+ context: z.ZodOptional<z.ZodUnknown>;
697
+ }, z.core.$loose>;
698
+ type KbPin = z.infer<typeof pinSchema>;
699
+ type KbPinsManifest = z.infer<typeof pinsManifestSchema>;
700
+ type KbContextBudgets = {
701
+ budgetTokens?: number;
702
+ fullUnderTokens?: number;
703
+ };
704
+ /** A pin as the merged view hands it back: entry + where it came from. */
705
+ type KbMergedPin = KbPin & {
706
+ layer: KbPinLayer;
707
+ absolutePath: string;
708
+ };
709
+ type KbMergedPins = {
710
+ /** Effective pins after dedup — nearest layer wins per resolved path. */
711
+ pins: KbMergedPin[];
712
+ /** Per-layer manifests that parsed, for budget merging. */
713
+ manifests: Partial<Record<KbPinLayer, KbPinsManifest>>;
714
+ };
715
+ /** One pinned base, with whether it currently resolves to anything readable. */
716
+ type KbPinStatus = {
717
+ /** As stored — relative to its layer's root. */
718
+ path: string;
719
+ layer: KbPinLayer;
720
+ pinnedAt: string | null;
721
+ absolutePath: string;
722
+ /** The directory exists and yielded at least one parseable record. */
723
+ valid: boolean;
724
+ recordCount: number;
725
+ mode: "full" | "index" | null;
726
+ profiles: string[] | null;
727
+ frozen: boolean;
728
+ };
729
+ type KbPinResult = {
730
+ path: string;
731
+ layer: KbPinLayer;
732
+ pinnedAt: string;
733
+ alreadyPinned: boolean;
734
+ mode?: "full" | "index";
735
+ profiles?: string[];
736
+ frozen?: boolean;
737
+ /** Set when the path holds no readable records — pinned anyway. */
738
+ warning?: string;
739
+ };
740
+ type KbPinOptions = {
741
+ mode?: "full" | "index";
742
+ profiles?: string[];
743
+ frozen?: boolean;
744
+ /** Which manifest to write. Defaults to the committed project layer. */
745
+ layer?: KbPinLayer;
746
+ };
747
+
748
+ /**
749
+ * One manifest's budgets for one profile: the named profile's values over the
750
+ * manifest's `"default"` entry. What is absent here falls through to the
751
+ * caller's built-ins — a manifest narrows, it never has to be complete.
752
+ */
753
+ declare function contextProfileBudgets(manifest: KbPinsManifest, profile?: string): KbContextBudgets;
754
+ /**
755
+ * Budgets across the layers: user underneath, local over it, project on top —
756
+ * the committed file is the workspace's word — and explicit flags above all
757
+ * of this, applied by the caller.
758
+ */
759
+ declare function mergedContextBudgets(merged: KbMergedPins, profile?: string): KbContextBudgets;
760
+
761
+ declare class KbPinsMalformedError extends Error {
762
+ constructor(file: string, cause: string);
763
+ }
764
+ declare class KbBaseFrozenError extends Error {
765
+ constructor(bundlePath: string, layer: KbPinLayer);
766
+ }
767
+
768
+ /**
769
+ * Refuses when a write would land in a base this workspace froze. Called by
770
+ * every mutating command; a workspace that pinned a base `--frozen` said the
771
+ * base is concluded, and a quiet write past that would be exactly the silent
772
+ * drift the pin was meant to stop.
773
+ */
774
+ declare function assertBaseNotFrozen(workspaceDir: string, bundlePath: string): Promise<void>;
775
+
776
+ /**
777
+ * One layer's manifest, or an empty one when the file is missing.
778
+ *
779
+ * A malformed file throws rather than being treated as empty: every write path
780
+ * does a full rewrite, and rewriting over content we could not read would
781
+ * destroy the one copy of it. Read-only consumers that must stay silent
782
+ * (`context` from a session hook, the merged reader) skip malformed layers
783
+ * themselves.
784
+ */
785
+ declare function readPinsLayer(workspaceDir: string, layer: KbPinLayer): Promise<KbPinsManifest>;
786
+ /** Where a stored pin points, resolved against its layer's root. */
787
+ declare function resolvePinPath(rootDir: string, path: string): string;
788
+ /**
789
+ * All three layers, merged. A malformed layer is skipped rather than thrown:
790
+ * this feeds hooks at every session start, and one broken personal file must
791
+ * not silence the team's pins — `pin`/`unpin` against the broken layer still
792
+ * refuse loudly.
793
+ */
794
+ declare function readMergedPins(workspaceDir: string): Promise<KbMergedPins>;
795
+
796
+ /** Every effective pin across the layers, with whether it points at records. */
797
+ declare function listPins(store: KbStore, workspaceDir: string): Promise<KbPinStatus[]>;
798
+
799
+ /**
800
+ * Adds a base to one layer's manifest. Idempotent — re-pinning a pinned path
801
+ * with no options returns the existing entry untouched, and re-pinning with
802
+ * `mode`, `profiles`, or `frozen` updates just those fields, which is how a
803
+ * pin's rendering or writability is changed. A path that is not (yet) a valid
804
+ * base succeeds with a warning: bases are routinely pinned before they are
805
+ * populated, the same way records link to records that do not exist yet.
806
+ */
807
+ declare function pinBase(store: KbStore, workspaceDir: string, bundlePath: string, at: string, options?: KbPinOptions): Promise<KbPinResult>;
808
+
809
+ /**
810
+ * Removes a base from every layer that holds it — unpinned means gone, not
811
+ * "gone from one file and still injected from another". A malformed layer is
812
+ * skipped (it cannot be rewritten safely); the layers actually touched are
813
+ * reported.
814
+ */
815
+ declare function unpinBase(workspaceDir: string, bundlePath: string): Promise<{
816
+ path: string;
817
+ removed: boolean;
818
+ layers: KbPinLayer[];
819
+ }>;
820
+
821
+ /**
822
+ * What the hooks ask for by name, so the numbers live in one place and a repo
823
+ * can override them in its pin manifest rather than editing hook commands.
824
+ * `session-start` is a fresh window — room for tiny bases to arrive whole.
825
+ * `compact` competes with a summary for a smaller window — index only.
826
+ * `turn` is per-turn injection (Antigravity) — same tight stance as compact.
827
+ */
828
+ declare const CONTEXT_PROFILES: Record<string, KbContextBudgets>;
829
+ type KbContextOptions = {
830
+ /** Refuse past this. Defaults to 4000 tokens. */
831
+ budgetTokens?: number;
832
+ /** Emit bases whose full `load` fits under this as records, not index. 0 = off. */
833
+ fullUnderTokens?: number;
834
+ /**
835
+ * A named budget set. Resolution, most specific wins: explicit options,
836
+ * then the manifest's `context[profile]` over its `context.default`, then
837
+ * the built-in profile, then the package defaults. An unknown profile is
838
+ * not an error — it simply falls through; hooks must never break over a
839
+ * name.
840
+ */
841
+ profile?: string;
842
+ /**
843
+ * Where budget pressure is reported outside the block itself: a full pin
844
+ * that had to degrade to an index, a block that refused. The block already
845
+ * says both to the agent; this says them to the operator's log.
846
+ */
847
+ warn?: (entry: Record<string, unknown>) => void;
848
+ };
849
+ type KbContextResult = {
850
+ /** The markdown block. Empty when there are no pins — silence, not a stub. */
851
+ block: string;
852
+ /** Refused: over budget. The block then lists the bases instead of the index. */
853
+ refused: boolean;
854
+ approxTokens: number;
855
+ budgetTokens: number;
856
+ bases: {
857
+ path: string;
858
+ absolutePath: string;
859
+ approxTokens: number;
860
+ }[];
861
+ };
862
+ /**
863
+ * Builds the block, or a refusal that lists the bases — never a truncation. A
864
+ * truncated index is indistinguishable from a complete one, so a reader would
865
+ * take a slice for the whole, which is `load`'s argument one layer up.
866
+ */
867
+ declare function buildContext(store: KbStore, workspaceDir: string, options?: KbContextOptions): Promise<KbContextResult>;
868
+ /**
869
+ * The same block in the envelope hook protocols that demand strict JSON on
870
+ * stdout require:
871
+ * those protocols treat non-JSON stdout as a violation, where Claude Code and
872
+ * Codex take plain text. One canonical writer for the block; this is wrapping.
873
+ */
874
+ declare function toHookJson(block: string, event: string): string;
875
+ declare const CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
876
+ declare const CONTEXT_END = "<!-- strauss-kb:end -->";
877
+ type KbSyncResult = {
878
+ file: string;
879
+ action: "created" | "replaced" | "appended" | "removed" | "unchanged";
880
+ };
881
+ /**
882
+ * Idempotently plants the block between sentinels in an instruction file
883
+ * (AGENTS.md, CLAUDE.md). This is how a runtime without a reliable
884
+ * post-compact hook keeps a refreshable index: the file is re-read where the
885
+ * conversation is not. Everything outside the sentinels is left alone.
886
+ */
887
+ declare function syncInstructions(file: string, block: string): Promise<KbSyncResult>;
888
+
637
889
  /**
638
890
  * The frontmatter contract, emitted rather than restated.
639
891
  *
@@ -762,6 +1014,7 @@ declare const decisionInputSchema: z.ZodObject<{
762
1014
  author: z.ZodOptional<z.ZodString>;
763
1015
  last_modified: z.ZodOptional<z.ZodString>;
764
1016
  }, z.core.$loose>>>;
1017
+ stale_after: z.ZodOptional<z.ZodString>;
765
1018
  slug: z.ZodString;
766
1019
  why: z.ZodString;
767
1020
  anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -802,15 +1055,17 @@ declare function selectDecisions(records: KbRecord[]): KbRecord[];
802
1055
  /**
803
1056
  * Every operation a knowledge base exposes, defined once.
804
1057
  *
805
- * The CLI and the MCP server are both projections of this list. Kept apart they
806
- * drift within a day — fourteen commands against six tools — which is the same
807
- * failure as a schema restated in prose beside the code that enforces it, one
808
- * level up. A command added here appears in both surfaces or in neither, and a
809
- * test asserts exactly that.
1058
+ * The CLI and the MCP server are both projections of this table. Kept apart
1059
+ * they drift within a day — fourteen commands against six tools — which is the
1060
+ * same failure as a schema restated in prose beside the code that enforces it,
1061
+ * one level up. A command added to the table appears in both surfaces or in
1062
+ * neither, and a test asserts exactly that.
810
1063
  *
811
1064
  * The two differ only in how arguments arrive: MCP passes an object matching
812
1065
  * `input`, while the CLI has to turn positional argv into the same object.
813
1066
  * `fromArgv` is that adapter and is the only per-surface code a command needs.
1067
+ *
1068
+ * One file per command in this folder; `index.ts` assembles the table.
814
1069
  */
815
1070
  type KbCommandContext = {
816
1071
  store: KbStore;
@@ -820,8 +1075,13 @@ type KbCommandContext = {
820
1075
  type KbCommand<Shape extends z.ZodRawShape = z.ZodRawShape> = {
821
1076
  /** CLI verb. */
822
1077
  name: string;
823
- /** MCP tool name. */
824
- tool: string;
1078
+ /**
1079
+ * MCP tool name. Absent only for CLI-only plumbing (`sync-instructions`),
1080
+ * which exists to edit files for hooks and instruction blocks rather than to
1081
+ * give an agent a capability — the capability, "get the pinned context
1082
+ * block", is `kb_context`.
1083
+ */
1084
+ tool?: string;
825
1085
  /** Argument spelling for CLI usage output. */
826
1086
  usage: string;
827
1087
  /** Shown to an agent choosing a tool, so it carries the judgment too. */
@@ -837,10 +1097,16 @@ type KbCommand<Shape extends z.ZodRawShape = z.ZodRawShape> = {
837
1097
  */
838
1098
  failsWhen?(result: unknown): boolean;
839
1099
  };
840
- declare const KB_COMMANDS: KbCommand<z.ZodRawShape>[];
841
- declare const KB_COMMANDS_BY_NAME: Map<string, KbCommand<Readonly<{
842
- [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
843
- }>>>;
1100
+
1101
+ /**
1102
+ * The command table, assembled from one file per command.
1103
+ *
1104
+ * Order is the CLI usage listing's order: the write path, the read path,
1105
+ * base housekeeping, the format, and the workspace pin verbs.
1106
+ */
1107
+
1108
+ declare const KB_COMMANDS: KbCommand[];
1109
+ declare const KB_COMMANDS_BY_NAME: Map<string, KbCommand>;
844
1110
 
845
1111
  /**
846
1112
  * A knowledge base's own MCP server, over stdio.
@@ -974,4 +1240,4 @@ declare function parseMarkdownWithFrontmatter<S extends z.ZodType>(text: string,
974
1240
  frontmatter: ReturnType<S["safeParse"]>;
975
1241
  };
976
1242
 
977
- export { BaseError, type ComposeInput, type ComposedRecord, DECISION_TYPE, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, type KbCommand, type KbCommandContext, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, type QmdModule, RECORD_TYPES, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, createKbMcpServer, decisionInputSchema, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, loadQmd, matchToDiff, parseLog, parseMarkdownWithFrontmatter, renderIndex, renderLogEntry, resolveHeads, resolveHits, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, trace, validateBundle };
1243
+ export { BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposedRecord, DECISION_TYPE, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, KbBaseFrozenError, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, assertBaseNotFrozen, buildContext, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, renderIndex, renderIndexLine, renderLogEntry, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
package/dist/index.js CHANGED
@@ -1,12 +1,15 @@
1
1
  import {
2
2
  runKbCli
3
- } from "./chunk-KGM34MYU.js";
3
+ } from "./chunk-QLTB77W4.js";
4
4
  import {
5
5
  createKbMcpServer,
6
6
  runKbMcpServer
7
- } from "./chunk-WFHYWZX5.js";
7
+ } from "./chunk-FSI4Q2FD.js";
8
8
  import {
9
9
  BaseError,
10
+ CONTEXT_BEGIN,
11
+ CONTEXT_END,
12
+ CONTEXT_PROFILES,
10
13
  DECISION_TYPE,
11
14
  ErrorTypes,
12
15
  Fault,
@@ -20,21 +23,29 @@ import {
20
23
  KB_RECORD_STATUSES,
21
24
  KB_RECORD_TYPES,
22
25
  KB_SLUG_PATTERN,
26
+ KbBaseFrozenError,
23
27
  KbInvalidConceptIdError,
28
+ KbPinsMalformedError,
24
29
  KbRecordAlreadyExistsError,
25
30
  KbRecordNotFoundError,
26
31
  KbStore,
27
32
  KbWriteConflictError,
28
33
  LOG_FILE,
29
34
  NO_DECISION_SLUG,
35
+ PINS_FILE,
36
+ PINS_LOCAL_FILE,
37
+ PIN_LAYERS,
30
38
  RECORD_TYPES,
31
39
  SEARCH_INDEX_FILE,
32
40
  TRACE_EDGES,
33
41
  adjudicate,
42
+ assertBaseNotFrozen,
43
+ buildContext,
34
44
  composeDecisionRecord,
35
45
  composeInputSchema,
36
46
  composeNoDecisionRecord,
37
47
  composeRecord,
48
+ contextProfileBudgets,
38
49
  decisionInputSchema,
39
50
  indexIsStale,
40
51
  isKbRecordType,
@@ -46,20 +57,30 @@ import {
46
57
  kbLogEntrySchema,
47
58
  kbRecordFrontmatterSchema,
48
59
  kbSourceSchema,
60
+ listPins,
49
61
  loadQmd,
62
+ mergedContextBudgets,
50
63
  parseLog,
51
64
  parseMarkdownWithFrontmatter,
65
+ pinBase,
66
+ readMergedPins,
67
+ readPinsLayer,
52
68
  renderIndex,
69
+ renderIndexLine,
53
70
  renderLogEntry,
54
71
  resolveHeads,
55
72
  resolveHits,
73
+ resolvePinPath,
56
74
  searchBase,
57
75
  selectDecisions,
58
76
  splitMarkdownFrontmatter,
59
77
  stringifyMarkdownWithFrontmatter,
78
+ syncInstructions,
79
+ toHookJson,
60
80
  trace,
81
+ unpinBase,
61
82
  validateBundle
62
- } from "./chunk-ZSYSHJVZ.js";
83
+ } from "./chunk-HYNAEAPM.js";
63
84
 
64
85
  // src/match-diff.ts
65
86
  function matchToDiff(files, records, options = {}) {
@@ -142,6 +163,9 @@ function normalize(path) {
142
163
  }
143
164
  export {
144
165
  BaseError,
166
+ CONTEXT_BEGIN,
167
+ CONTEXT_END,
168
+ CONTEXT_PROFILES,
145
169
  DECISION_TYPE,
146
170
  ErrorTypes,
147
171
  Fault,
@@ -155,21 +179,29 @@ export {
155
179
  KB_RECORD_STATUSES,
156
180
  KB_RECORD_TYPES,
157
181
  KB_SLUG_PATTERN,
182
+ KbBaseFrozenError,
158
183
  KbInvalidConceptIdError,
184
+ KbPinsMalformedError,
159
185
  KbRecordAlreadyExistsError,
160
186
  KbRecordNotFoundError,
161
187
  KbStore,
162
188
  KbWriteConflictError,
163
189
  LOG_FILE,
164
190
  NO_DECISION_SLUG,
191
+ PINS_FILE,
192
+ PINS_LOCAL_FILE,
193
+ PIN_LAYERS,
165
194
  RECORD_TYPES,
166
195
  SEARCH_INDEX_FILE,
167
196
  TRACE_EDGES,
168
197
  adjudicate,
198
+ assertBaseNotFrozen,
199
+ buildContext,
169
200
  composeDecisionRecord,
170
201
  composeInputSchema,
171
202
  composeNoDecisionRecord,
172
203
  composeRecord,
204
+ contextProfileBudgets,
173
205
  createKbMcpServer,
174
206
  decisionInputSchema,
175
207
  indexIsStale,
@@ -182,21 +214,31 @@ export {
182
214
  kbLogEntrySchema,
183
215
  kbRecordFrontmatterSchema,
184
216
  kbSourceSchema,
217
+ listPins,
185
218
  loadQmd,
186
219
  matchToDiff,
220
+ mergedContextBudgets,
187
221
  parseLog,
188
222
  parseMarkdownWithFrontmatter,
223
+ pinBase,
224
+ readMergedPins,
225
+ readPinsLayer,
189
226
  renderIndex,
227
+ renderIndexLine,
190
228
  renderLogEntry,
191
229
  resolveHeads,
192
230
  resolveHits,
231
+ resolvePinPath,
193
232
  runKbCli,
194
233
  runKbMcpServer,
195
234
  searchBase,
196
235
  selectDecisions,
197
236
  splitMarkdownFrontmatter,
198
237
  stringifyMarkdownWithFrontmatter,
238
+ syncInstructions,
239
+ toHookJson,
199
240
  trace,
241
+ unpinBase,
200
242
  validateBundle
201
243
  };
202
244
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/match-diff.ts"],"sourcesContent":["import { adjudicate, type KbAdjudicated } from \"./adjudicate.js\";\nimport type { KbAnchor, KbRecord } from \"./kb-record.schema.js\";\n\n/**\n * Which records apply to which part of a change.\n *\n * Takes a structural description of a diff rather than a patch, so this package\n * carries no diff parser: callers already have one, and a knowledge base has no\n * business preferring a particular flavour of unified diff.\n *\n * Deterministic on purpose. Every step here is mechanical — the one judgment,\n * whether a matched record is worth showing a reviewer, is deliberately absent.\n * A model placed here would sit between the reviewer and their diff on every\n * review, to answer a question nobody has yet shown needs asking.\n *\n * Distinct from `load()`, which hands a reader the whole base. That answers\n * \"does anything address this question\"; this answers \"what is attached to this\n * code\", and an anchor is the author's own statement rather than an inference\n * from one. A reader guessing which record relates to a hunk would be guessing\n * at something already written down — and a diff has dozens of hunks, which is\n * dozens of reader calls against microseconds of matching. Where they compose:\n * this narrows a hunk to a few records, and a reader asked to explain them gets\n * those, not the base.\n */\nexport type DiffHunk = {\n /** 1-based, inclusive, in the file's post-change line numbering. */\n startLine: number;\n endLine: number;\n};\n\nexport type DiffFile = {\n /** Repo-relative, matching how anchors are written. */\n filePath: string;\n hunks: DiffHunk[];\n};\n\n/**\n * A symbol resolved to lines. Supplied by whatever the caller uses to index\n * symbols; absence is tolerated — see `place()`.\n */\nexport type SymbolRange = {\n file: string;\n symbol: string;\n startLine: number;\n endLine: number;\n};\n\nexport type DiffMatch = {\n filePath: string;\n hunk: DiffHunk;\n /** Current records first — what still holds should be read before what does not. */\n records: KbAdjudicated[];\n /**\n * `symbol` when every record here was placed by a resolved symbol range,\n * `file` when at least one fell back to the whole file. Reported rather than\n * hidden: a caller showing a file-level match as though it were pinned to\n * these lines is claiming a precision it does not have.\n */\n precision: \"symbol\" | \"file\";\n};\n\nexport type MatchOptions = {\n /** Without these, symbol anchors degrade to file level rather than vanishing. */\n symbolRanges?: SymbolRange[];\n now?: Date;\n};\n\nexport function matchToDiff(\n files: DiffFile[],\n records: KbRecord[],\n options: MatchOptions = {},\n): DiffMatch[] {\n const ranges = indexRanges(options.symbolRanges ?? []);\n const anchored = records.filter(\n (record) => (record.frontmatter.strauss_anchors ?? []).length > 0,\n );\n const matches: DiffMatch[] = [];\n\n for (const file of files) {\n const candidates = anchored\n .map((record) => ({\n record,\n anchors: (record.frontmatter.strauss_anchors ?? []).filter(\n (anchor) => normalize(anchor.file) === normalize(file.filePath),\n ),\n }))\n .filter(({ anchors }) => anchors.length > 0);\n if (!candidates.length) continue;\n\n for (const hunk of file.hunks) {\n const hits: KbRecord[] = [];\n let precision: DiffMatch[\"precision\"] = \"symbol\";\n\n for (const { record, anchors } of candidates) {\n const placement = place(anchors, file.filePath, hunk, ranges);\n if (placement === \"miss\") continue;\n if (placement === \"file\") precision = \"file\";\n hits.push(record);\n }\n\n if (!hits.length) continue;\n matches.push({\n filePath: file.filePath,\n hunk,\n records: order(adjudicate(hits, records, options.now)),\n precision,\n });\n }\n }\n\n return matches;\n}\n\n/**\n * Whether any of a record's anchors puts it on this hunk.\n *\n * An anchor naming only a file is about the whole file, so it lands on every\n * hunk in it. One naming a symbol lands only where that symbol's lines overlap\n * — unless nothing resolved the symbol, in which case it falls back to the file\n * rather than disappearing. A record silently absent because a resolver was\n * unavailable is worse than one shown imprecisely and labelled as such.\n */\nfunction place(\n anchors: KbAnchor[],\n filePath: string,\n hunk: DiffHunk,\n ranges: Map<string, SymbolRange[]>,\n): \"symbol\" | \"file\" | \"miss\" {\n let fallback: \"file\" | \"miss\" = \"miss\";\n\n for (const anchor of anchors) {\n if (!anchor.symbol) return \"file\";\n\n const resolved = ranges.get(key(filePath, anchor.symbol));\n if (!resolved?.length) {\n fallback = \"file\";\n continue;\n }\n if (resolved.some((range) => overlaps(range, hunk))) return \"symbol\";\n }\n\n return fallback;\n}\n\nfunction overlaps(range: SymbolRange, hunk: DiffHunk): boolean {\n return range.startLine <= hunk.endLine && hunk.startLine <= range.endLine;\n}\n\n/** Current before superseded, then oldest first, so an arc reads in order. */\nfunction order(records: KbAdjudicated[]): KbAdjudicated[] {\n const rank: Record<string, number> = {\n current: 0,\n unsettled: 1,\n open: 2,\n superseded: 3,\n rejected: 4,\n };\n return [...records].sort(\n (left, right) =>\n (rank[left.standing] ?? 9) - (rank[right.standing] ?? 9) ||\n (left.record.frontmatter.generated?.at ?? \"\").localeCompare(\n right.record.frontmatter.generated?.at ?? \"\",\n ),\n );\n}\n\nfunction indexRanges(ranges: SymbolRange[]): Map<string, SymbolRange[]> {\n const byKey = new Map<string, SymbolRange[]>();\n for (const range of ranges) {\n const id = key(range.file, range.symbol);\n byKey.set(id, [...(byKey.get(id) ?? []), range]);\n }\n return byKey;\n}\n\nfunction key(file: string, symbol: string): string {\n return `${normalize(file)}#${symbol}`;\n}\n\n/** Anchors are written by hand often enough that `./` shows up. */\nfunction normalize(path: string): string {\n return path.replace(/^\\.\\//, \"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEO,SAAS,YACd,OACA,SACA,UAAwB,CAAC,GACZ;AACb,QAAM,SAAS,YAAY,QAAQ,gBAAgB,CAAC,CAAC;AACrD,QAAM,WAAW,QAAQ;AAAA,IACvB,CAAC,YAAY,OAAO,YAAY,mBAAmB,CAAC,GAAG,SAAS;AAAA,EAClE;AACA,QAAM,UAAuB,CAAC;AAE9B,aAAW,QAAQ,OAAO;AACxB,UAAM,aAAa,SAChB,IAAI,CAAC,YAAY;AAAA,MAChB;AAAA,MACA,UAAU,OAAO,YAAY,mBAAmB,CAAC,GAAG;AAAA,QAClD,CAAC,WAAW,UAAU,OAAO,IAAI,MAAM,UAAU,KAAK,QAAQ;AAAA,MAChE;AAAA,IACF,EAAE,EACD,OAAO,CAAC,EAAE,QAAQ,MAAM,QAAQ,SAAS,CAAC;AAC7C,QAAI,CAAC,WAAW,OAAQ;AAExB,eAAW,QAAQ,KAAK,OAAO;AAC7B,YAAM,OAAmB,CAAC;AAC1B,UAAI,YAAoC;AAExC,iBAAW,EAAE,QAAQ,QAAQ,KAAK,YAAY;AAC5C,cAAM,YAAY,MAAM,SAAS,KAAK,UAAU,MAAM,MAAM;AAC5D,YAAI,cAAc,OAAQ;AAC1B,YAAI,cAAc,OAAQ,aAAY;AACtC,aAAK,KAAK,MAAM;AAAA,MAClB;AAEA,UAAI,CAAC,KAAK,OAAQ;AAClB,cAAQ,KAAK;AAAA,QACX,UAAU,KAAK;AAAA,QACf;AAAA,QACA,SAAS,MAAM,WAAW,MAAM,SAAS,QAAQ,GAAG,CAAC;AAAA,QACrD;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAWA,SAAS,MACP,SACA,UACA,MACA,QAC4B;AAC5B,MAAI,WAA4B;AAEhC,aAAW,UAAU,SAAS;AAC5B,QAAI,CAAC,OAAO,OAAQ,QAAO;AAE3B,UAAM,WAAW,OAAO,IAAI,IAAI,UAAU,OAAO,MAAM,CAAC;AACxD,QAAI,CAAC,UAAU,QAAQ;AACrB,iBAAW;AACX;AAAA,IACF;AACA,QAAI,SAAS,KAAK,CAAC,UAAU,SAAS,OAAO,IAAI,CAAC,EAAG,QAAO;AAAA,EAC9D;AAEA,SAAO;AACT;AAEA,SAAS,SAAS,OAAoB,MAAyB;AAC7D,SAAO,MAAM,aAAa,KAAK,WAAW,KAAK,aAAa,MAAM;AACpE;AAGA,SAAS,MAAM,SAA2C;AACxD,QAAM,OAA+B;AAAA,IACnC,SAAS;AAAA,IACT,WAAW;AAAA,IACX,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,UAAU;AAAA,EACZ;AACA,SAAO,CAAC,GAAG,OAAO,EAAE;AAAA,IAClB,CAAC,MAAM,WACJ,KAAK,KAAK,QAAQ,KAAK,MAAM,KAAK,MAAM,QAAQ,KAAK,OACrD,KAAK,OAAO,YAAY,WAAW,MAAM,IAAI;AAAA,MAC5C,MAAM,OAAO,YAAY,WAAW,MAAM;AAAA,IAC5C;AAAA,EACJ;AACF;AAEA,SAAS,YAAY,QAAmD;AACtE,QAAM,QAAQ,oBAAI,IAA2B;AAC7C,aAAW,SAAS,QAAQ;AAC1B,UAAM,KAAK,IAAI,MAAM,MAAM,MAAM,MAAM;AACvC,UAAM,IAAI,IAAI,CAAC,GAAI,MAAM,IAAI,EAAE,KAAK,CAAC,GAAI,KAAK,CAAC;AAAA,EACjD;AACA,SAAO;AACT;AAEA,SAAS,IAAI,MAAc,QAAwB;AACjD,SAAO,GAAG,UAAU,IAAI,CAAC,IAAI,MAAM;AACrC;AAGA,SAAS,UAAU,MAAsB;AACvC,SAAO,KAAK,QAAQ,SAAS,EAAE;AACjC;","names":[]}
1
+ {"version":3,"sources":["../src/match-diff.ts"],"sourcesContent":["import { adjudicate, type KbAdjudicated } from \"./adjudicate.js\";\nimport type { KbAnchor, KbRecord } from \"./kb-record.schema.js\";\n\n/**\n * Which records apply to which part of a change.\n *\n * Takes a structural description of a diff rather than a patch, so this package\n * carries no diff parser: callers already have one, and a knowledge base has no\n * business preferring a particular flavour of unified diff.\n *\n * Deterministic on purpose. Every step here is mechanical — the one judgment,\n * whether a matched record is worth showing a reviewer, is deliberately absent.\n * A model placed here would sit between the reviewer and their diff on every\n * review, to answer a question nobody has yet shown needs asking.\n *\n * Distinct from `load()`, which hands a reader the whole base. That answers\n * \"does anything address this question\"; this answers \"what is attached to this\n * code\", and an anchor is the author's own statement rather than an inference\n * from one. A reader guessing which record relates to a hunk would be guessing\n * at something already written down — and a diff has dozens of hunks, which is\n * dozens of reader calls against microseconds of matching. Where they compose:\n * this narrows a hunk to a few records, and a reader asked to explain them gets\n * those, not the base.\n */\nexport type DiffHunk = {\n /** 1-based, inclusive, in the file's post-change line numbering. */\n startLine: number;\n endLine: number;\n};\n\nexport type DiffFile = {\n /** Repo-relative, matching how anchors are written. */\n filePath: string;\n hunks: DiffHunk[];\n};\n\n/**\n * A symbol resolved to lines. Supplied by whatever the caller uses to index\n * symbols; absence is tolerated — see `place()`.\n */\nexport type SymbolRange = {\n file: string;\n symbol: string;\n startLine: number;\n endLine: number;\n};\n\nexport type DiffMatch = {\n filePath: string;\n hunk: DiffHunk;\n /** Current records first — what still holds should be read before what does not. */\n records: KbAdjudicated[];\n /**\n * `symbol` when every record here was placed by a resolved symbol range,\n * `file` when at least one fell back to the whole file. Reported rather than\n * hidden: a caller showing a file-level match as though it were pinned to\n * these lines is claiming a precision it does not have.\n */\n precision: \"symbol\" | \"file\";\n};\n\nexport type MatchOptions = {\n /** Without these, symbol anchors degrade to file level rather than vanishing. */\n symbolRanges?: SymbolRange[];\n now?: Date;\n};\n\nexport function matchToDiff(\n files: DiffFile[],\n records: KbRecord[],\n options: MatchOptions = {},\n): DiffMatch[] {\n const ranges = indexRanges(options.symbolRanges ?? []);\n const anchored = records.filter(\n (record) => (record.frontmatter.strauss_anchors ?? []).length > 0,\n );\n const matches: DiffMatch[] = [];\n\n for (const file of files) {\n const candidates = anchored\n .map((record) => ({\n record,\n anchors: (record.frontmatter.strauss_anchors ?? []).filter(\n (anchor) => normalize(anchor.file) === normalize(file.filePath),\n ),\n }))\n .filter(({ anchors }) => anchors.length > 0);\n if (!candidates.length) continue;\n\n for (const hunk of file.hunks) {\n const hits: KbRecord[] = [];\n let precision: DiffMatch[\"precision\"] = \"symbol\";\n\n for (const { record, anchors } of candidates) {\n const placement = place(anchors, file.filePath, hunk, ranges);\n if (placement === \"miss\") continue;\n if (placement === \"file\") precision = \"file\";\n hits.push(record);\n }\n\n if (!hits.length) continue;\n matches.push({\n filePath: file.filePath,\n hunk,\n records: order(adjudicate(hits, records, options.now)),\n precision,\n });\n }\n }\n\n return matches;\n}\n\n/**\n * Whether any of a record's anchors puts it on this hunk.\n *\n * An anchor naming only a file is about the whole file, so it lands on every\n * hunk in it. One naming a symbol lands only where that symbol's lines overlap\n * — unless nothing resolved the symbol, in which case it falls back to the file\n * rather than disappearing. A record silently absent because a resolver was\n * unavailable is worse than one shown imprecisely and labelled as such.\n */\nfunction place(\n anchors: KbAnchor[],\n filePath: string,\n hunk: DiffHunk,\n ranges: Map<string, SymbolRange[]>,\n): \"symbol\" | \"file\" | \"miss\" {\n let fallback: \"file\" | \"miss\" = \"miss\";\n\n for (const anchor of anchors) {\n if (!anchor.symbol) return \"file\";\n\n const resolved = ranges.get(key(filePath, anchor.symbol));\n if (!resolved?.length) {\n fallback = \"file\";\n continue;\n }\n if (resolved.some((range) => overlaps(range, hunk))) return \"symbol\";\n }\n\n return fallback;\n}\n\nfunction overlaps(range: SymbolRange, hunk: DiffHunk): boolean {\n return range.startLine <= hunk.endLine && hunk.startLine <= range.endLine;\n}\n\n/** Current before superseded, then oldest first, so an arc reads in order. */\nfunction order(records: KbAdjudicated[]): KbAdjudicated[] {\n const rank: Record<string, number> = {\n current: 0,\n unsettled: 1,\n open: 2,\n superseded: 3,\n rejected: 4,\n };\n return [...records].sort(\n (left, right) =>\n (rank[left.standing] ?? 9) - (rank[right.standing] ?? 9) ||\n (left.record.frontmatter.generated?.at ?? \"\").localeCompare(\n right.record.frontmatter.generated?.at ?? \"\",\n ),\n );\n}\n\nfunction indexRanges(ranges: SymbolRange[]): Map<string, SymbolRange[]> {\n const byKey = new Map<string, SymbolRange[]>();\n for (const range of ranges) {\n const id = key(range.file, range.symbol);\n byKey.set(id, [...(byKey.get(id) ?? []), range]);\n }\n return byKey;\n}\n\nfunction key(file: string, symbol: string): string {\n return `${normalize(file)}#${symbol}`;\n}\n\n/** Anchors are written by hand often enough that `./` shows up. */\nfunction normalize(path: string): string {\n return path.replace(/^\\.\\//, \"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEO,SAAS,YACd,OACA,SACA,UAAwB,CAAC,GACZ;AACb,QAAM,SAAS,YAAY,QAAQ,gBAAgB,CAAC,CAAC;AACrD,QAAM,WAAW,QAAQ;AAAA,IACvB,CAAC,YAAY,OAAO,YAAY,mBAAmB,CAAC,GAAG,SAAS;AAAA,EAClE;AACA,QAAM,UAAuB,CAAC;AAE9B,aAAW,QAAQ,OAAO;AACxB,UAAM,aAAa,SAChB,IAAI,CAAC,YAAY;AAAA,MAChB;AAAA,MACA,UAAU,OAAO,YAAY,mBAAmB,CAAC,GAAG;AAAA,QAClD,CAAC,WAAW,UAAU,OAAO,IAAI,MAAM,UAAU,KAAK,QAAQ;AAAA,MAChE;AAAA,IACF,EAAE,EACD,OAAO,CAAC,EAAE,QAAQ,MAAM,QAAQ,SAAS,CAAC;AAC7C,QAAI,CAAC,WAAW,OAAQ;AAExB,eAAW,QAAQ,KAAK,OAAO;AAC7B,YAAM,OAAmB,CAAC;AAC1B,UAAI,YAAoC;AAExC,iBAAW,EAAE,QAAQ,QAAQ,KAAK,YAAY;AAC5C,cAAM,YAAY,MAAM,SAAS,KAAK,UAAU,MAAM,MAAM;AAC5D,YAAI,cAAc,OAAQ;AAC1B,YAAI,cAAc,OAAQ,aAAY;AACtC,aAAK,KAAK,MAAM;AAAA,MAClB;AAEA,UAAI,CAAC,KAAK,OAAQ;AAClB,cAAQ,KAAK;AAAA,QACX,UAAU,KAAK;AAAA,QACf;AAAA,QACA,SAAS,MAAM,WAAW,MAAM,SAAS,QAAQ,GAAG,CAAC;AAAA,QACrD;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAWA,SAAS,MACP,SACA,UACA,MACA,QAC4B;AAC5B,MAAI,WAA4B;AAEhC,aAAW,UAAU,SAAS;AAC5B,QAAI,CAAC,OAAO,OAAQ,QAAO;AAE3B,UAAM,WAAW,OAAO,IAAI,IAAI,UAAU,OAAO,MAAM,CAAC;AACxD,QAAI,CAAC,UAAU,QAAQ;AACrB,iBAAW;AACX;AAAA,IACF;AACA,QAAI,SAAS,KAAK,CAAC,UAAU,SAAS,OAAO,IAAI,CAAC,EAAG,QAAO;AAAA,EAC9D;AAEA,SAAO;AACT;AAEA,SAAS,SAAS,OAAoB,MAAyB;AAC7D,SAAO,MAAM,aAAa,KAAK,WAAW,KAAK,aAAa,MAAM;AACpE;AAGA,SAAS,MAAM,SAA2C;AACxD,QAAM,OAA+B;AAAA,IACnC,SAAS;AAAA,IACT,WAAW;AAAA,IACX,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,UAAU;AAAA,EACZ;AACA,SAAO,CAAC,GAAG,OAAO,EAAE;AAAA,IAClB,CAAC,MAAM,WACJ,KAAK,KAAK,QAAQ,KAAK,MAAM,KAAK,MAAM,QAAQ,KAAK,OACrD,KAAK,OAAO,YAAY,WAAW,MAAM,IAAI;AAAA,MAC5C,MAAM,OAAO,YAAY,WAAW,MAAM;AAAA,IAC5C;AAAA,EACJ;AACF;AAEA,SAAS,YAAY,QAAmD;AACtE,QAAM,QAAQ,oBAAI,IAA2B;AAC7C,aAAW,SAAS,QAAQ;AAC1B,UAAM,KAAK,IAAI,MAAM,MAAM,MAAM,MAAM;AACvC,UAAM,IAAI,IAAI,CAAC,GAAI,MAAM,IAAI,EAAE,KAAK,CAAC,GAAI,KAAK,CAAC;AAAA,EACjD;AACA,SAAO;AACT;AAEA,SAAS,IAAI,MAAc,QAAwB;AACjD,SAAO,GAAG,UAAU,IAAI,CAAC,IAAI,MAAM;AACrC;AAGA,SAAS,UAAU,MAAsB;AACvC,SAAO,KAAK,QAAQ,SAAS,EAAE;AACjC;","names":[]}