zam-core 0.7.0 → 0.7.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.
@@ -147,6 +147,12 @@ zam token find --query "<keywords>"
147
147
  ```
148
148
  Only register genuinely new concepts. Reuse existing slugs where the concept matches. Note that `zam token find` matches paraphrases semantically.
149
149
 
150
+ After deduplication and before registering a new token, check for existing semantically related tokens that could be prerequisites ("foundations"):
151
+ ```bash
152
+ echo '{"concept":"<concept>","question":"<question>","domain":"<domain>"}' | zam bridge suggest-foundations
153
+ ```
154
+ Present non-flagged suggestions to the user ("Related existing concept X — link it as a foundation?"); on approval link via the existing prereq path after registering.
155
+
150
156
  **Register tokens and prerequisites:**
151
157
 
152
158
  As the frontier model, YOU author both the concept and the recall question. The
@@ -371,6 +377,12 @@ A token should be decomposed when ALL of the following hold:
371
377
  After a rating of 1, pause. Do not just re-ask the same question or move on.
372
378
  Ask yourself: **"What would the user have needed to know to answer this?"**
373
379
 
380
+ Always check for existing related foundation tokens first:
381
+ ```bash
382
+ echo '{"slug":"<failed-token-slug>"}' | zam bridge suggest-foundations
383
+ ```
384
+ Offer existing tokens returned as suggestions to the user first (linking them feeds the existing `confirmFoundations` path with `exists: true` and `slug`). Only generate NEW foundation proposals via the LLM for gaps that the suggestions do not cover.
385
+
374
386
  | Symptom | Missing foundation | Create Bloom 1-2 token for |
375
387
  |---------|-------------------|---------------------------|
376
388
  | Couldn't name key terms | Factual recall | Definitions, terminology |
@@ -178,6 +178,12 @@ zam token find --query "<keywords>"
178
178
  ```
179
179
  Only register genuinely new concepts. Reuse existing slugs where the concept matches. Note that `zam token find` matches paraphrases semantically.
180
180
 
181
+ After deduplication and before registering a new token, check for existing semantically related tokens that could be prerequisites ("foundations"):
182
+ ```bash
183
+ echo '{"concept":"<concept>","question":"<question>","domain":"<domain>"}' | zam bridge suggest-foundations
184
+ ```
185
+ Present non-flagged suggestions to the user ("Related existing concept X — link it as a foundation?"); on approval link via the existing prereq path after registering.
186
+
181
187
  **Register tokens and prerequisites:**
182
188
 
183
189
  As the frontier model, author both the concept and the recall question. The
@@ -467,6 +473,12 @@ A token should be decomposed when ALL of the following hold:
467
473
  After a rating of 1, pause. Do not just re-ask the same question or move on.
468
474
  Ask yourself: **"What would the user have needed to know to answer this?"**
469
475
 
476
+ Always check for existing related foundation tokens first:
477
+ ```bash
478
+ echo '{"slug":"<failed-token-slug>"}' | zam bridge suggest-foundations
479
+ ```
480
+ Offer existing tokens returned as suggestions to the user first (linking them feeds the existing `confirmFoundations` path with `exists: true` and `slug`). Only generate NEW foundation proposals via the LLM for gaps that the suggestions do not cover.
481
+
470
482
  | Symptom | Missing foundation | Create Bloom 1-2 token for |
471
483
  |---------|-------------------|---------------------------|
472
484
  | Couldn't name key terms | Factual recall | Definitions, terminology |
@@ -147,6 +147,12 @@ zam token find --query "<keywords>"
147
147
  ```
148
148
  Only register genuinely new concepts. Reuse existing slugs where the concept matches. Note that `zam token find` matches paraphrases semantically.
149
149
 
150
+ After deduplication and before registering a new token, check for existing semantically related tokens that could be prerequisites ("foundations"):
151
+ ```bash
152
+ echo '{"concept":"<concept>","question":"<question>","domain":"<domain>"}' | zam bridge suggest-foundations
153
+ ```
154
+ Present non-flagged suggestions to the user ("Related existing concept X — link it as a foundation?"); on approval link via the existing prereq path after registering.
155
+
150
156
  **Register tokens and prerequisites:**
151
157
 
152
158
  As the frontier model, YOU author both the concept and the recall question. The
@@ -371,6 +377,12 @@ A token should be decomposed when ALL of the following hold:
371
377
  After a rating of 1, pause. Do not just re-ask the same question or move on.
372
378
  Ask yourself: **"What would the user have needed to know to answer this?"**
373
379
 
380
+ Always check for existing related foundation tokens first:
381
+ ```bash
382
+ echo '{"slug":"<failed-token-slug>"}' | zam bridge suggest-foundations
383
+ ```
384
+ Offer existing tokens returned as suggestions to the user first (linking them feeds the existing `confirmFoundations` path with `exists: true` and `slug`). Only generate NEW foundation proposals via the LLM for gaps that the suggestions do not cover.
385
+
374
386
  | Symptom | Missing foundation | Create Bloom 1-2 token for |
375
387
  |---------|-------------------|---------------------------|
376
388
  | Couldn't name key terms | Factual recall | Definitions, terminology |
package/dist/cli/index.js CHANGED
@@ -2219,9 +2219,9 @@ async function buildAncestorMap(db) {
2219
2219
  }
2220
2220
  return map;
2221
2221
  }
2222
- async function wouldCreateCycle(db, tokenId, requiresId) {
2222
+ async function wouldCreateCycle(db, tokenId, requiresId, ancestors) {
2223
2223
  if (tokenId === requiresId) return true;
2224
- const ancestors = await buildAncestorMap(db);
2224
+ const map = ancestors ?? await buildAncestorMap(db);
2225
2225
  const visited = /* @__PURE__ */ new Set();
2226
2226
  const queue = [requiresId];
2227
2227
  while (queue.length > 0) {
@@ -2229,7 +2229,7 @@ async function wouldCreateCycle(db, tokenId, requiresId) {
2229
2229
  if (current === tokenId) return true;
2230
2230
  if (visited.has(current)) continue;
2231
2231
  visited.add(current);
2232
- const parents = ancestors.get(current);
2232
+ const parents = map.get(current);
2233
2233
  if (parents) {
2234
2234
  for (const parent of parents) {
2235
2235
  if (!visited.has(parent)) queue.push(parent);
@@ -4789,6 +4789,71 @@ async function searchTokensHybrid(db, query, opts) {
4789
4789
  return results.slice(0, limit);
4790
4790
  }
4791
4791
 
4792
+ // src/kernel/search/suggestions.ts
4793
+ async function suggestFoundations(db, opts) {
4794
+ const minSimilarity = opts.minSimilarity ?? 0.45;
4795
+ const maxSimilarity = opts.maxSimilarity ?? 0.85;
4796
+ const limit = opts.limit ?? 5;
4797
+ const targetBloomLevel = opts.targetBloomLevel ?? 5;
4798
+ if (minSimilarity >= maxSimilarity) {
4799
+ return [];
4800
+ }
4801
+ const embedded = await listEmbeddedTokens(db, opts.model);
4802
+ if (embedded.length === 0) {
4803
+ return [];
4804
+ }
4805
+ const queryVec = Float32Array.from(opts.queryEmbedding);
4806
+ const candidates = [];
4807
+ for (const row of embedded) {
4808
+ if (opts.targetTokenId && row.token.id === opts.targetTokenId) {
4809
+ continue;
4810
+ }
4811
+ const similarity = cosineSimilarity(queryVec, row.embedding);
4812
+ if (similarity >= minSimilarity && similarity < maxSimilarity) {
4813
+ candidates.push({ token: row.token, similarity });
4814
+ }
4815
+ }
4816
+ candidates.sort((a, b) => {
4817
+ if (Math.abs(a.similarity - b.similarity) > 1e-9) {
4818
+ return b.similarity - a.similarity;
4819
+ }
4820
+ return a.token.slug.localeCompare(b.token.slug);
4821
+ });
4822
+ const topCandidates = candidates.slice(0, limit);
4823
+ const prereqIds = /* @__PURE__ */ new Set();
4824
+ let ancestors;
4825
+ if (opts.targetTokenId) {
4826
+ const prereqs = await getPrerequisites(db, opts.targetTokenId);
4827
+ for (const p of prereqs) {
4828
+ prereqIds.add(p.requires_id);
4829
+ }
4830
+ ancestors = await buildAncestorMap(db);
4831
+ }
4832
+ const results = [];
4833
+ for (const cand of topCandidates) {
4834
+ let alreadyPrerequisite = false;
4835
+ let wouldCreateCycleFlag = false;
4836
+ const bloomAboveTarget = cand.token.bloom_level > targetBloomLevel;
4837
+ if (opts.targetTokenId) {
4838
+ alreadyPrerequisite = prereqIds.has(cand.token.id);
4839
+ wouldCreateCycleFlag = await wouldCreateCycle(
4840
+ db,
4841
+ opts.targetTokenId,
4842
+ cand.token.id,
4843
+ ancestors
4844
+ );
4845
+ }
4846
+ results.push({
4847
+ token: cand.token,
4848
+ similarity: cand.similarity,
4849
+ alreadyPrerequisite,
4850
+ wouldCreateCycle: wouldCreateCycleFlag,
4851
+ bloomAboveTarget
4852
+ });
4853
+ }
4854
+ return results;
4855
+ }
4856
+
4792
4857
  // src/kernel/system/hooks.ts
4793
4858
  import {
4794
4859
  appendFileSync as appendFileSync3,
@@ -10559,9 +10624,7 @@ async function findPossibleDuplicates(db, candidate, embed = embedQuery) {
10559
10624
  );
10560
10625
  }
10561
10626
  }
10562
- const thresholdStr = await getSetting(db, "search.dedup_threshold");
10563
- const parsed = thresholdStr ? Number.parseFloat(thresholdStr) : Number.NaN;
10564
- const threshold = Number.isFinite(parsed) && parsed > 0 && parsed <= 1 ? parsed : 0.85;
10627
+ const threshold = await resolveDedupThreshold(db);
10565
10628
  const hits = await searchTokensHybrid(db, queryText, {
10566
10629
  queryEmbedding: q2.vector,
10567
10630
  model: q2.model,
@@ -10580,6 +10643,16 @@ async function findPossibleDuplicates(db, candidate, embed = embedQuery) {
10580
10643
  }
10581
10644
  return results;
10582
10645
  }
10646
+ async function resolveDedupThreshold(db) {
10647
+ const thresholdStr = await getSetting(db, "search.dedup_threshold");
10648
+ const parsed = thresholdStr ? Number.parseFloat(thresholdStr) : Number.NaN;
10649
+ return Number.isFinite(parsed) && parsed > 0 && parsed <= 1 ? parsed : 0.85;
10650
+ }
10651
+ async function resolveSuggestMinSimilarity(db) {
10652
+ const minStr = await getSetting(db, "search.suggest_min_similarity");
10653
+ const parsed = minStr ? Number.parseFloat(minStr) : Number.NaN;
10654
+ return Number.isFinite(parsed) && parsed > 0 && parsed <= 1 ? parsed : 0.45;
10655
+ }
10583
10656
 
10584
10657
  // src/cli/llm/vision.ts
10585
10658
  import { randomBytes } from "crypto";
@@ -12428,6 +12501,117 @@ bridgeCommand.command("relevant-tokens").description("Find tokens relevant to a
12428
12501
  });
12429
12502
  });
12430
12503
  });
12504
+ bridgeCommand.command("suggest-foundations").description("Propose existing tokens as foundation/prerequisite candidates").option("--user <id>", "User ID (default: whoami)").action(async (_opts) => {
12505
+ await withDb2(async (db) => {
12506
+ let raw;
12507
+ if (isServeMode) {
12508
+ raw = serveStdinPayload ?? "";
12509
+ } else {
12510
+ const chunks = [];
12511
+ for await (const chunk of process.stdin) {
12512
+ chunks.push(chunk);
12513
+ }
12514
+ raw = Buffer.concat(chunks).toString("utf-8").trim();
12515
+ }
12516
+ if (!raw) {
12517
+ jsonError("No input received on stdin. Pipe JSON.");
12518
+ }
12519
+ let data;
12520
+ try {
12521
+ data = JSON.parse(raw);
12522
+ } catch {
12523
+ jsonError("Invalid JSON input");
12524
+ }
12525
+ let queryText = "";
12526
+ let targetTokenId;
12527
+ let targetBloomLevel;
12528
+ let targetJson = null;
12529
+ if (data?.slug !== void 0) {
12530
+ if (typeof data.slug !== "string" || data.slug.trim() === "") {
12531
+ jsonError("Invalid slug");
12532
+ }
12533
+ const token = await getTokenBySlug(db, data.slug);
12534
+ if (!token) {
12535
+ jsonError(`Token not found: ${data.slug}`);
12536
+ }
12537
+ queryText = embeddingContentForToken(token);
12538
+ targetTokenId = token.id;
12539
+ targetBloomLevel = token.bloom_level;
12540
+ targetJson = { slug: token.slug };
12541
+ } else {
12542
+ if (!data?.concept || typeof data.concept !== "string" || data.concept.trim() === "") {
12543
+ jsonError("JSON must include a non-empty 'slug' or 'concept' field");
12544
+ }
12545
+ queryText = embeddingContentForToken({
12546
+ concept: data.concept,
12547
+ question: typeof data.question === "string" ? data.question : null,
12548
+ domain: typeof data.domain === "string" ? data.domain : ""
12549
+ });
12550
+ if (data.bloom_level !== void 0) {
12551
+ if (typeof data.bloom_level !== "number" || !Number.isInteger(data.bloom_level) || data.bloom_level < 1 || data.bloom_level > 5) {
12552
+ jsonError("bloom_level must be an integer between 1 and 5");
12553
+ }
12554
+ targetBloomLevel = data.bloom_level;
12555
+ }
12556
+ }
12557
+ let limit = data?.limit ?? 5;
12558
+ if (typeof limit !== "number" || limit <= 0 || !Number.isInteger(limit)) {
12559
+ limit = 5;
12560
+ }
12561
+ if (limit > 20) {
12562
+ limit = 20;
12563
+ }
12564
+ const q2 = await embedQuery(db, queryText);
12565
+ if (q2 === null) {
12566
+ jsonOut2({
12567
+ semantic: false,
12568
+ target: targetJson,
12569
+ suggestions: []
12570
+ });
12571
+ return;
12572
+ }
12573
+ try {
12574
+ await ensureTokenEmbeddings(db, {
12575
+ limit: 100,
12576
+ dims: q2.vector.length
12577
+ });
12578
+ } catch {
12579
+ }
12580
+ const maxSimilarity = await resolveDedupThreshold(db);
12581
+ const minSimilarity = await resolveSuggestMinSimilarity(db);
12582
+ if (minSimilarity >= maxSimilarity) {
12583
+ jsonOut2({
12584
+ semantic: true,
12585
+ target: targetJson,
12586
+ suggestions: []
12587
+ });
12588
+ return;
12589
+ }
12590
+ const suggestions = await suggestFoundations(db, {
12591
+ queryEmbedding: q2.vector,
12592
+ model: q2.model,
12593
+ targetTokenId,
12594
+ targetBloomLevel,
12595
+ limit,
12596
+ minSimilarity,
12597
+ maxSimilarity
12598
+ });
12599
+ jsonOut2({
12600
+ semantic: true,
12601
+ target: targetJson,
12602
+ suggestions: suggestions.map((s) => ({
12603
+ slug: s.token.slug,
12604
+ concept: s.token.concept,
12605
+ domain: s.token.domain,
12606
+ bloom_level: s.token.bloom_level,
12607
+ similarity: s.similarity,
12608
+ already_prerequisite: s.alreadyPrerequisite,
12609
+ would_create_cycle: s.wouldCreateCycle,
12610
+ bloom_above_target: s.bloomAboveTarget
12611
+ }))
12612
+ });
12613
+ });
12614
+ });
12431
12615
  bridgeCommand.command("discover-skills").description(
12432
12616
  "Analyze monitor logs across sessions to discover recurring patterns"
12433
12617
  ).option(
@@ -17452,6 +17636,46 @@ WARNING: Possible duplicate tokens found:`);
17452
17636
  );
17453
17637
  }
17454
17638
  }
17639
+ try {
17640
+ const queryText = embeddingContentForToken({
17641
+ concept: opts.concept,
17642
+ question: question ?? null,
17643
+ domain: opts.domain ?? ""
17644
+ });
17645
+ const q2 = await embedQuery(db, queryText);
17646
+ if (q2) {
17647
+ const maxSimilarity = await resolveDedupThreshold(db);
17648
+ const minSimilarity = await resolveSuggestMinSimilarity(db);
17649
+ const suggestions = await suggestFoundations(db, {
17650
+ queryEmbedding: q2.vector,
17651
+ model: q2.model,
17652
+ targetTokenId: token.id,
17653
+ targetBloomLevel: Number(opts.bloom),
17654
+ limit: 3,
17655
+ minSimilarity,
17656
+ maxSimilarity
17657
+ });
17658
+ const filtered = suggestions.filter(
17659
+ (s) => !s.wouldCreateCycle && !s.alreadyPrerequisite
17660
+ );
17661
+ if (filtered.length > 0) {
17662
+ console.log(
17663
+ `
17664
+ Related existing tokens as potential foundations:`
17665
+ );
17666
+ for (const s of filtered) {
17667
+ const note = s.bloomAboveTarget ? " (higher bloom than target)" : "";
17668
+ console.log(
17669
+ ` - ${s.token.slug} (similarity: ${s.similarity.toFixed(2)})${note}`
17670
+ );
17671
+ }
17672
+ console.log(
17673
+ `Link with: zam token prereq --token <slug> --requires <slug> (see zam token prereq --help)`
17674
+ );
17675
+ }
17676
+ }
17677
+ } catch {
17678
+ }
17455
17679
  }
17456
17680
  });
17457
17681
  });