@seanhogg/builderforce-memory 2026.10.28 → 2026.10.29

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.
Files changed (253) hide show
  1. package/dist/ingest/IngestionPipeline.d.ts +1 -8
  2. package/dist/ingest/IngestionPipeline.d.ts.map +1 -1
  3. package/dist/ingest/IngestionPipeline.js +3 -15
  4. package/dist/ingest/IngestionPipeline.js.map +1 -1
  5. package/dist/ingest/docsExtract.d.ts +30 -0
  6. package/dist/ingest/docsExtract.d.ts.map +1 -0
  7. package/dist/ingest/docsExtract.js +221 -0
  8. package/dist/ingest/docsExtract.js.map +1 -0
  9. package/dist/ingest/hash.d.ts +15 -0
  10. package/dist/ingest/hash.d.ts.map +1 -0
  11. package/dist/ingest/hash.js +22 -0
  12. package/dist/ingest/hash.js.map +1 -0
  13. package/dist/ingest/parsers.d.ts.map +1 -1
  14. package/dist/ingest/parsers.js +4 -0
  15. package/dist/ingest/parsers.js.map +1 -1
  16. package/dist/ingest/types.d.ts +7 -0
  17. package/dist/ingest/types.d.ts.map +1 -1
  18. package/dist/ingest/types.js.map +1 -1
  19. package/dist/knowledge/build.d.ts +24 -0
  20. package/dist/knowledge/build.d.ts.map +1 -0
  21. package/dist/knowledge/build.js +263 -0
  22. package/dist/knowledge/build.js.map +1 -0
  23. package/dist/knowledge/cards.d.ts +17 -0
  24. package/dist/knowledge/cards.d.ts.map +1 -0
  25. package/dist/knowledge/cards.js +118 -0
  26. package/dist/knowledge/cards.js.map +1 -0
  27. package/dist/knowledge/findings.d.ts +24 -0
  28. package/dist/knowledge/findings.d.ts.map +1 -0
  29. package/dist/knowledge/findings.js +152 -0
  30. package/dist/knowledge/findings.js.map +1 -0
  31. package/dist/knowledge/importance.d.ts +21 -0
  32. package/dist/knowledge/importance.d.ts.map +1 -0
  33. package/dist/knowledge/importance.js +70 -0
  34. package/dist/knowledge/importance.js.map +1 -0
  35. package/dist/knowledge/index.d.ts +31 -0
  36. package/dist/knowledge/index.d.ts.map +1 -0
  37. package/dist/knowledge/index.js +26 -0
  38. package/dist/knowledge/index.js.map +1 -0
  39. package/dist/knowledge/judge.d.ts +86 -0
  40. package/dist/knowledge/judge.d.ts.map +1 -0
  41. package/dist/knowledge/judge.js +213 -0
  42. package/dist/knowledge/judge.js.map +1 -0
  43. package/dist/knowledge/model.d.ts +333 -0
  44. package/dist/knowledge/model.d.ts.map +1 -0
  45. package/dist/knowledge/model.js +13 -0
  46. package/dist/knowledge/model.js.map +1 -0
  47. package/dist/knowledge/packs/docs.d.ts +17 -0
  48. package/dist/knowledge/packs/docs.d.ts.map +1 -0
  49. package/dist/knowledge/packs/docs.js +89 -0
  50. package/dist/knowledge/packs/docs.js.map +1 -0
  51. package/dist/knowledge/partitions.d.ts +31 -0
  52. package/dist/knowledge/partitions.d.ts.map +1 -0
  53. package/dist/knowledge/partitions.js +135 -0
  54. package/dist/knowledge/partitions.js.map +1 -0
  55. package/dist/knowledge/plan.d.ts +33 -0
  56. package/dist/knowledge/plan.d.ts.map +1 -0
  57. package/dist/knowledge/plan.js +89 -0
  58. package/dist/knowledge/plan.js.map +1 -0
  59. package/dist/knowledge/refs.d.ts +40 -0
  60. package/dist/knowledge/refs.d.ts.map +1 -0
  61. package/dist/knowledge/refs.js +102 -0
  62. package/dist/knowledge/refs.js.map +1 -0
  63. package/dist/knowledge/resolve.d.ts +35 -0
  64. package/dist/knowledge/resolve.d.ts.map +1 -0
  65. package/dist/knowledge/resolve.js +67 -0
  66. package/dist/knowledge/resolve.js.map +1 -0
  67. package/dist/knowledge/review.d.ts +23 -0
  68. package/dist/knowledge/review.d.ts.map +1 -0
  69. package/dist/knowledge/review.js +233 -0
  70. package/dist/knowledge/review.js.map +1 -0
  71. package/dist/knowledge/sources.d.ts +47 -0
  72. package/dist/knowledge/sources.d.ts.map +1 -0
  73. package/dist/knowledge/sources.js +136 -0
  74. package/dist/knowledge/sources.js.map +1 -0
  75. package/dist/knowledge/stores/localSnapshotStore.d.ts +50 -0
  76. package/dist/knowledge/stores/localSnapshotStore.d.ts.map +1 -0
  77. package/dist/knowledge/stores/localSnapshotStore.js +125 -0
  78. package/dist/knowledge/stores/localSnapshotStore.js.map +1 -0
  79. package/dist/knowledge/trace.d.ts +32 -0
  80. package/dist/knowledge/trace.d.ts.map +1 -0
  81. package/dist/knowledge/trace.js +102 -0
  82. package/dist/knowledge/trace.js.map +1 -0
  83. package/dist/knowledge/truncate.d.ts +24 -0
  84. package/dist/knowledge/truncate.d.ts.map +1 -0
  85. package/dist/knowledge/truncate.js +128 -0
  86. package/dist/knowledge/truncate.js.map +1 -0
  87. package/dist/memory/SemanticRecall.d.ts.map +1 -1
  88. package/dist/memory/SemanticRecall.js +3 -1
  89. package/dist/memory/SemanticRecall.js.map +1 -1
  90. package/dist/retrieval/HybridRetriever.d.ts.map +1 -1
  91. package/dist/retrieval/HybridRetriever.js +3 -1
  92. package/dist/retrieval/HybridRetriever.js.map +1 -1
  93. package/dist/retrieval/bm25.d.ts +3 -2
  94. package/dist/retrieval/bm25.d.ts.map +1 -1
  95. package/dist/retrieval/bm25.js +5 -3
  96. package/dist/retrieval/bm25.js.map +1 -1
  97. package/dist/retrieval/fusion.d.ts +2 -1
  98. package/dist/retrieval/fusion.d.ts.map +1 -1
  99. package/dist/retrieval/fusion.js +4 -2
  100. package/dist/retrieval/fusion.js.map +1 -1
  101. package/dist/retrieval/hnsw.d.ts +49 -11
  102. package/dist/retrieval/hnsw.d.ts.map +1 -1
  103. package/dist/retrieval/hnsw.js +57 -29
  104. package/dist/retrieval/hnsw.js.map +1 -1
  105. package/dist/retrieval/index.d.ts +7 -3
  106. package/dist/retrieval/index.d.ts.map +1 -1
  107. package/dist/retrieval/index.js +5 -2
  108. package/dist/retrieval/index.js.map +1 -1
  109. package/dist/retrieval/knowledge-index/bytes.d.ts +43 -0
  110. package/dist/retrieval/knowledge-index/bytes.d.ts.map +1 -0
  111. package/dist/retrieval/knowledge-index/bytes.js +151 -0
  112. package/dist/retrieval/knowledge-index/bytes.js.map +1 -0
  113. package/dist/retrieval/knowledge-index/cascade.d.ts +37 -0
  114. package/dist/retrieval/knowledge-index/cascade.d.ts.map +1 -0
  115. package/dist/retrieval/knowledge-index/cascade.js +111 -0
  116. package/dist/retrieval/knowledge-index/cascade.js.map +1 -0
  117. package/dist/retrieval/knowledge-index/compact.d.ts +18 -0
  118. package/dist/retrieval/knowledge-index/compact.d.ts.map +1 -0
  119. package/dist/retrieval/knowledge-index/compact.js +64 -0
  120. package/dist/retrieval/knowledge-index/compact.js.map +1 -0
  121. package/dist/retrieval/knowledge-index/csr.d.ts +41 -0
  122. package/dist/retrieval/knowledge-index/csr.d.ts.map +1 -0
  123. package/dist/retrieval/knowledge-index/csr.js +76 -0
  124. package/dist/retrieval/knowledge-index/csr.js.map +1 -0
  125. package/dist/retrieval/knowledge-index/digest.d.ts +21 -0
  126. package/dist/retrieval/knowledge-index/digest.d.ts.map +1 -0
  127. package/dist/retrieval/knowledge-index/digest.js +50 -0
  128. package/dist/retrieval/knowledge-index/digest.js.map +1 -0
  129. package/dist/retrieval/knowledge-index/embeddings.d.ts +32 -0
  130. package/dist/retrieval/knowledge-index/embeddings.d.ts.map +1 -0
  131. package/dist/retrieval/knowledge-index/embeddings.js +78 -0
  132. package/dist/retrieval/knowledge-index/embeddings.js.map +1 -0
  133. package/dist/retrieval/knowledge-index/expand.d.ts +19 -0
  134. package/dist/retrieval/knowledge-index/expand.d.ts.map +1 -0
  135. package/dist/retrieval/knowledge-index/expand.js +49 -0
  136. package/dist/retrieval/knowledge-index/expand.js.map +1 -0
  137. package/dist/retrieval/knowledge-index/index.d.ts +40 -0
  138. package/dist/retrieval/knowledge-index/index.d.ts.map +1 -0
  139. package/dist/retrieval/knowledge-index/index.js +134 -0
  140. package/dist/retrieval/knowledge-index/index.js.map +1 -0
  141. package/dist/retrieval/knowledge-index/lexical.d.ts +26 -0
  142. package/dist/retrieval/knowledge-index/lexical.d.ts.map +1 -0
  143. package/dist/retrieval/knowledge-index/lexical.js +76 -0
  144. package/dist/retrieval/knowledge-index/lexical.js.map +1 -0
  145. package/dist/retrieval/knowledge-index/pairs.d.ts +22 -0
  146. package/dist/retrieval/knowledge-index/pairs.d.ts.map +1 -0
  147. package/dist/retrieval/knowledge-index/pairs.js +51 -0
  148. package/dist/retrieval/knowledge-index/pairs.js.map +1 -0
  149. package/dist/retrieval/knowledge-index/prune.d.ts +21 -0
  150. package/dist/retrieval/knowledge-index/prune.d.ts.map +1 -0
  151. package/dist/retrieval/knowledge-index/prune.js +43 -0
  152. package/dist/retrieval/knowledge-index/prune.js.map +1 -0
  153. package/dist/retrieval/knowledge-index/quantize.d.ts +37 -0
  154. package/dist/retrieval/knowledge-index/quantize.d.ts.map +1 -0
  155. package/dist/retrieval/knowledge-index/quantize.js +77 -0
  156. package/dist/retrieval/knowledge-index/quantize.js.map +1 -0
  157. package/dist/retrieval/knowledge-index/serialize.d.ts +18 -0
  158. package/dist/retrieval/knowledge-index/serialize.d.ts.map +1 -0
  159. package/dist/retrieval/knowledge-index/serialize.js +123 -0
  160. package/dist/retrieval/knowledge-index/serialize.js.map +1 -0
  161. package/dist/retrieval/knowledge-index/sha256.d.ts +30 -0
  162. package/dist/retrieval/knowledge-index/sha256.d.ts.map +1 -0
  163. package/dist/retrieval/knowledge-index/sha256.js +130 -0
  164. package/dist/retrieval/knowledge-index/sha256.js.map +1 -0
  165. package/dist/retrieval/knowledge-index/state.d.ts +55 -0
  166. package/dist/retrieval/knowledge-index/state.d.ts.map +1 -0
  167. package/dist/retrieval/knowledge-index/state.js +67 -0
  168. package/dist/retrieval/knowledge-index/state.js.map +1 -0
  169. package/dist/retrieval/knowledge-index/tokenize.d.ts +20 -0
  170. package/dist/retrieval/knowledge-index/tokenize.d.ts.map +1 -0
  171. package/dist/retrieval/knowledge-index/tokenize.js +42 -0
  172. package/dist/retrieval/knowledge-index/tokenize.js.map +1 -0
  173. package/dist/retrieval/knowledge-index/traverse.d.ts +26 -0
  174. package/dist/retrieval/knowledge-index/traverse.d.ts.map +1 -0
  175. package/dist/retrieval/knowledge-index/traverse.js +90 -0
  176. package/dist/retrieval/knowledge-index/traverse.js.map +1 -0
  177. package/dist/retrieval/knowledge-index/types.d.ts +85 -0
  178. package/dist/retrieval/knowledge-index/types.d.ts.map +1 -0
  179. package/dist/retrieval/knowledge-index/types.js +9 -0
  180. package/dist/retrieval/knowledge-index/types.js.map +1 -0
  181. package/dist/retrieval/mmr.d.ts +11 -4
  182. package/dist/retrieval/mmr.d.ts.map +1 -1
  183. package/dist/retrieval/mmr.js +28 -8
  184. package/dist/retrieval/mmr.js.map +1 -1
  185. package/dist/retrieval/order.d.ts +41 -0
  186. package/dist/retrieval/order.d.ts.map +1 -0
  187. package/dist/retrieval/order.js +54 -0
  188. package/dist/retrieval/order.js.map +1 -0
  189. package/dist/retrieval/weightedFusion.d.ts +2 -1
  190. package/dist/retrieval/weightedFusion.d.ts.map +1 -1
  191. package/dist/retrieval/weightedFusion.js +7 -5
  192. package/dist/retrieval/weightedFusion.js.map +1 -1
  193. package/dist/vectorstore/MemoryVectorStore.d.ts.map +1 -1
  194. package/dist/vectorstore/MemoryVectorStore.js +2 -1
  195. package/dist/vectorstore/MemoryVectorStore.js.map +1 -1
  196. package/package.json +8 -3
  197. package/src/ingest/IngestionPipeline.ts +3 -15
  198. package/src/ingest/docsExtract.ts +219 -0
  199. package/src/ingest/hash.ts +22 -0
  200. package/src/ingest/parsers.ts +4 -0
  201. package/src/ingest/types.ts +7 -0
  202. package/src/knowledge/build.test.ts +150 -0
  203. package/src/knowledge/build.ts +266 -0
  204. package/src/knowledge/cards.ts +130 -0
  205. package/src/knowledge/docs.test.ts +118 -0
  206. package/src/knowledge/findings.ts +159 -0
  207. package/src/knowledge/importance.ts +72 -0
  208. package/src/knowledge/index.ts +53 -0
  209. package/src/knowledge/judge.ts +231 -0
  210. package/src/knowledge/model.ts +312 -0
  211. package/src/knowledge/packs/docs.ts +91 -0
  212. package/src/knowledge/partitions.test.ts +98 -0
  213. package/src/knowledge/partitions.ts +119 -0
  214. package/src/knowledge/plan.ts +115 -0
  215. package/src/knowledge/refs.ts +108 -0
  216. package/src/knowledge/resolve.ts +78 -0
  217. package/src/knowledge/review.test.ts +180 -0
  218. package/src/knowledge/review.ts +235 -0
  219. package/src/knowledge/sources.ts +146 -0
  220. package/src/knowledge/stores/localSnapshotStore.test.ts +82 -0
  221. package/src/knowledge/stores/localSnapshotStore.ts +159 -0
  222. package/src/knowledge/testkit.test-util.ts +92 -0
  223. package/src/knowledge/trace.test.ts +91 -0
  224. package/src/knowledge/trace.ts +111 -0
  225. package/src/knowledge/truncate.ts +118 -0
  226. package/src/memory/SemanticRecall.ts +3 -1
  227. package/src/retrieval/HybridRetriever.ts +3 -1
  228. package/src/retrieval/bm25.ts +5 -3
  229. package/src/retrieval/fusion.ts +5 -2
  230. package/src/retrieval/hnsw.ts +82 -35
  231. package/src/retrieval/index.ts +17 -3
  232. package/src/retrieval/knowledge-index/bytes.ts +166 -0
  233. package/src/retrieval/knowledge-index/cascade.ts +125 -0
  234. package/src/retrieval/knowledge-index/compact.ts +68 -0
  235. package/src/retrieval/knowledge-index/csr.ts +102 -0
  236. package/src/retrieval/knowledge-index/digest.ts +54 -0
  237. package/src/retrieval/knowledge-index/embeddings.ts +85 -0
  238. package/src/retrieval/knowledge-index/expand.ts +55 -0
  239. package/src/retrieval/knowledge-index/index.ts +162 -0
  240. package/src/retrieval/knowledge-index/lexical.ts +78 -0
  241. package/src/retrieval/knowledge-index/pairs.ts +57 -0
  242. package/src/retrieval/knowledge-index/prune.ts +48 -0
  243. package/src/retrieval/knowledge-index/quantize.ts +78 -0
  244. package/src/retrieval/knowledge-index/serialize.ts +124 -0
  245. package/src/retrieval/knowledge-index/sha256.ts +122 -0
  246. package/src/retrieval/knowledge-index/state.ts +108 -0
  247. package/src/retrieval/knowledge-index/tokenize.ts +41 -0
  248. package/src/retrieval/knowledge-index/traverse.ts +95 -0
  249. package/src/retrieval/knowledge-index/types.ts +78 -0
  250. package/src/retrieval/mmr.ts +35 -10
  251. package/src/retrieval/order.ts +68 -0
  252. package/src/retrieval/weightedFusion.ts +8 -5
  253. package/src/vectorstore/MemoryVectorStore.ts +2 -1
@@ -0,0 +1,91 @@
1
+ /**
2
+ * knowledge/packs/docs.ts — the docs pack, built into memory (PRD 38 §4.5).
3
+ *
4
+ * Parsers are the ONE builtin ingest parsers (markdown, HTML, plain text); their `extract`
5
+ * lives in `ingest/docsExtract.ts`. Partitions: one plane per top-level folder, no `allow`
6
+ * (so no verdicts). Findings: broken links and orphan documents. Judgement: claims,
7
+ * entities and contradictions per section.
8
+ */
9
+
10
+ import { htmlParser, markdownParser, plainTextParser } from '../../ingest/parsers.js';
11
+ import { DOCS_PACK_ID } from '../../ingest/docsExtract.js';
12
+ import type { Finding, FindingRule, KnowledgeGraph, KnowledgePack, NodeRef } from '../model.js';
13
+ import { findingId, MAX_FINDING_NODES, requestKeyFor } from '../findings.js';
14
+ import { TOP_LEVEL_FOLDER } from '../partitions.js';
15
+ import { basenameOf, compareStr, fileRefOf, pathOfRef } from '../refs.js';
16
+
17
+ export { DOCS_PACK_ID };
18
+
19
+ /** Targets with these extensions (or none) are documents; a link to anything else is not judged broken. */
20
+ const DOC_TARGET = /(?:\.(?:md|markdown|mdx|html?|txt|rst|adoc)|\/[^./]*|^[^./]*)$/i;
21
+ /** Entry documents are expected to have no inbound links. */
22
+ const ENTRY_DOC = /^(readme|index|home|_index|summary)\.[a-z]+$/i;
23
+
24
+ function fileOf(g: KnowledgeGraph): Map<NodeRef, NodeRef> {
25
+ return new Map(g.nodes.map((n) => [n.ref, fileRefOf(n.ref)] as const));
26
+ }
27
+
28
+ /** Links to documents that are not in the directory, grouped per linking file. */
29
+ export const brokenLinkRule: FindingRule = {
30
+ id: 'docs.broken-link',
31
+ find(g) {
32
+ const kind = new Map(g.nodes.map((n) => [n.ref, n.kind] as const));
33
+ const file = fileOf(g);
34
+ const bySource = new Map<NodeRef, NodeRef[]>();
35
+ for (const e of g.edges) {
36
+ if (e.kind !== 'links' || kind.get(e.to) !== 'missing') continue;
37
+ if (!DOC_TARGET.test(pathOfRef(e.to) ?? '')) continue;
38
+ const source = file.get(e.from) ?? e.from;
39
+ const list = bySource.get(source) ?? [];
40
+ list.push(e.to);
41
+ bySource.set(source, list);
42
+ }
43
+ return [...bySource].sort((a, b) => compareStr(a[0], b[0])).map(([source, targets]): Finding => {
44
+ const unique = [...new Set(targets)].sort(compareStr);
45
+ const nodes = [source, ...unique];
46
+ return {
47
+ id: findingId('broken-link', nodes), kind: 'broken-link', pack: DOCS_PACK_ID,
48
+ nodes: nodes.slice(0, MAX_FINDING_NODES), severity: 'warn',
49
+ params: { count: unique.length, file: pathOfRef(source) ?? source },
50
+ requestKey: requestKeyFor('broken-link'),
51
+ };
52
+ });
53
+ },
54
+ };
55
+
56
+ /** Documents no other document links to (entry documents aside), as one finding. */
57
+ export const orphanDocumentRule: FindingRule = {
58
+ id: 'docs.orphan',
59
+ find(g) {
60
+ const file = fileOf(g);
61
+ const linked = new Set<NodeRef>();
62
+ for (const e of g.edges) {
63
+ if (e.kind !== 'links') continue;
64
+ const from = file.get(e.from) ?? e.from, to = file.get(e.to) ?? e.to;
65
+ if (from !== to) linked.add(to);
66
+ }
67
+ const orphans = g.nodes
68
+ .filter((n) => n.pack === DOCS_PACK_ID && n.kind === 'file' && !linked.has(n.ref))
69
+ .filter((n) => !ENTRY_DOC.test(basenameOf(pathOfRef(n.ref) ?? '')))
70
+ .map((n) => n.ref).sort(compareStr);
71
+ if (orphans.length === 0) return [];
72
+ return [{
73
+ id: findingId('orphan', orphans), kind: 'orphan', pack: DOCS_PACK_ID,
74
+ nodes: orphans.slice(0, MAX_FINDING_NODES), severity: 'info',
75
+ params: { count: orphans.length }, requestKey: requestKeyFor('orphan'),
76
+ }];
77
+ },
78
+ };
79
+
80
+ export const DOCS_PACK: KnowledgePack = {
81
+ id: DOCS_PACK_ID,
82
+ parsers: [markdownParser, htmlParser, plainTextParser],
83
+ partitions: {
84
+ partitions: [
85
+ { id: TOP_LEVEL_FOLDER, order: 0, match: ['*/**'] },
86
+ { id: '(root)', order: 0, match: ['*'] },
87
+ ],
88
+ },
89
+ findings: [brokenLinkRule, orphanDocumentRule],
90
+ judgement: { nodeKinds: ['section'], claims: true, entities: true, contradictions: true },
91
+ };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * src/knowledge/partitions.test.ts
3
+ * The verdict table, glob matching, top-level-folder partitions and override validation.
4
+ */
5
+
6
+ import { globToRegExp, loadPartitions, matchGlob, partitionOf, verdictOf } from './partitions.js';
7
+ import type { KnowledgePack, PartitionRegistry } from './model.js';
8
+ import { DOCS_PACK } from './packs/docs.js';
9
+
10
+ const LAYERS: PartitionRegistry = {
11
+ partitions: [
12
+ { id: 'presentation', order: 0, match: ['**/routes/**', '**/components/**'] },
13
+ { id: 'application', order: 1, match: ['**/application/**'] },
14
+ { id: 'domain', order: 2, match: ['**/domain/**'] },
15
+ { id: 'infrastructure', order: 3, match: ['**/infrastructure/**', '**/db/**'] },
16
+ ],
17
+ allow: {
18
+ presentation: ['application', 'domain'],
19
+ application: ['domain'],
20
+ domain: [],
21
+ infrastructure: ['domain', 'application'],
22
+ },
23
+ };
24
+
25
+ describe('verdictOf — the table', () => {
26
+ it.each([
27
+ ['presentation', 'presentation', 'same'],
28
+ ['presentation', 'application', 'allowed'],
29
+ ['presentation', 'domain', 'allowed'],
30
+ ['infrastructure', 'application', 'allowed'],
31
+ ['domain', 'presentation', 'upward'],
32
+ ['application', 'presentation', 'upward'],
33
+ ['infrastructure', 'presentation', 'upward'],
34
+ ['presentation', 'infrastructure', 'skips'],
35
+ ['application', 'infrastructure', 'skips'],
36
+ ['domain', 'infrastructure', 'skips'],
37
+ ])('%s → %s is %s', (from, to, verdict) => {
38
+ expect(verdictOf(from, to, LAYERS)).toBe(verdict);
39
+ });
40
+
41
+ it('gives no verdict without allow, or with an unpartitioned end', () => {
42
+ expect(verdictOf('presentation', 'domain', { partitions: LAYERS.partitions })).toBeUndefined();
43
+ expect(verdictOf(undefined, 'domain', LAYERS)).toBeUndefined();
44
+ expect(verdictOf('domain', undefined, LAYERS)).toBeUndefined();
45
+ });
46
+ });
47
+
48
+ describe('globs and partitionOf', () => {
49
+ it('matches ** across folders and * within one', () => {
50
+ expect(matchGlob('api/src/routes/x.ts', '**/routes/**')).toBe(true);
51
+ expect(matchGlob('routes/x.ts', '**/routes/**')).toBe(true);
52
+ expect(matchGlob('api/src/router/x.ts', '**/routes/**')).toBe(false);
53
+ expect(matchGlob('a/b.md', '*')).toBe(false);
54
+ expect(matchGlob('b.md', '*')).toBe(true);
55
+ expect(matchGlob('x.ts', '*.{ts,tsx}')).toBe(true);
56
+ expect(globToRegExp('a.b').test('aXb')).toBe(false);
57
+ });
58
+
59
+ it('takes the first matching rule', () => {
60
+ expect(partitionOf('api/db/routes/q.ts', LAYERS)).toBe('presentation');
61
+ expect(partitionOf('api/db/q.ts', LAYERS)).toBe('infrastructure');
62
+ expect(partitionOf('README.md', LAYERS)).toBeUndefined();
63
+ });
64
+
65
+ it('the docs pack partitions by top-level folder, with no verdicts', () => {
66
+ const reg = loadPartitions(DOCS_PACK);
67
+ expect(partitionOf('contracts/lease.md', reg)).toBe('contracts');
68
+ expect(partitionOf('contracts/deep/x.md', reg)).toBe('contracts');
69
+ expect(partitionOf('README.md', reg)).toBe('(root)');
70
+ expect(verdictOf('contracts', 'policy', reg)).toBeUndefined();
71
+ });
72
+ });
73
+
74
+ describe('loadPartitions', () => {
75
+ const pack: KnowledgePack = { id: 'code', parsers: [], partitions: LAYERS };
76
+
77
+ it('returns the pack default without an override', () => {
78
+ expect(loadPartitions(pack)).toBe(LAYERS);
79
+ expect(loadPartitions({ id: 'x', parsers: [] })).toEqual({ partitions: [] });
80
+ });
81
+
82
+ it('accepts a valid override', () => {
83
+ const reg = loadPartitions(pack, { partitions: [{ id: 'ui', order: 0, match: ['ui/**'] }, { id: 'core', order: 1, match: ['core/**'] }], allow: { ui: ['core'] } });
84
+ expect(reg.allow).toEqual({ ui: ['core'] });
85
+ expect(partitionOf('ui/a.ts', reg)).toBe('ui');
86
+ });
87
+
88
+ it.each([
89
+ ['not an object', 'nope'],
90
+ ['no partitions', {}],
91
+ ['bad order', { partitions: [{ id: 'a', order: 'x', match: ['a/**'] }] }],
92
+ ['bad match', { partitions: [{ id: 'a', order: 0, match: [1] }] }],
93
+ ['duplicate ids', { partitions: [{ id: 'a', order: 0, match: ['a'] }, { id: 'a', order: 1, match: ['b'] }] }],
94
+ ['unknown allow target', { partitions: [{ id: 'a', order: 0, match: ['a'] }], allow: { a: ['b'] } }],
95
+ ])('rejects an override with %s', (_why, override) => {
96
+ expect(() => loadPartitions(pack, override)).toThrow(TypeError);
97
+ });
98
+ });
@@ -0,0 +1,119 @@
1
+ /**
2
+ * knowledge/partitions.ts — ordered groups of sources and who may use whom (PRD 38 §4.4).
3
+ *
4
+ * Generic: code layers and doc folders are both a {@link PartitionRegistry}. An edge's
5
+ * verdict is `same`, `allowed`, `upward` (to a lower order, i.e. a layer above) or
6
+ * `skips` (anything else), and is set only when the registry defines `allow` and both
7
+ * ends are partitioned.
8
+ *
9
+ * A rule whose id is {@link TOP_LEVEL_FOLDER} names its partition after the path's first
10
+ * folder — the docs pack's "one plane per top-level folder" with no per-folder rows.
11
+ */
12
+
13
+ import type { EdgeVerdict, KnowledgePack, PartitionRegistry, PartitionRule } from './model.js';
14
+
15
+ /** Rule id meaning "the matched path's top-level folder". */
16
+ export const TOP_LEVEL_FOLDER = '*';
17
+
18
+ const globCache = new Map<string, RegExp>();
19
+
20
+ /**
21
+ * A glob as a RegExp over `/`-separated paths: `**` spans folders (`**\/` may match none),
22
+ * `*` and `?` stay within one segment, `{a,b}` is alternation.
23
+ */
24
+ export function globToRegExp(glob: string): RegExp {
25
+ const cached = globCache.get(glob);
26
+ if (cached) return cached;
27
+ let re = '';
28
+ for (let i = 0; i < glob.length; i++) {
29
+ const ch = glob[i]!;
30
+ if (ch === '*') {
31
+ if (glob[i + 1] === '*') {
32
+ const slashAfter = glob[i + 2] === '/';
33
+ re += slashAfter ? '(?:.*/)?' : '.*';
34
+ i += slashAfter ? 2 : 1;
35
+ } else re += '[^/]*';
36
+ } else if (ch === '?') re += '[^/]';
37
+ else if (ch === '{') {
38
+ const close = glob.indexOf('}', i);
39
+ if (close < 0) { re += '\\{'; continue; }
40
+ re += `(?:${glob.slice(i + 1, close).split(',').map(escapeRe).join('|')})`;
41
+ i = close;
42
+ } else re += escapeRe(ch);
43
+ }
44
+ const compiled = new RegExp(`^${re}$`);
45
+ globCache.set(glob, compiled);
46
+ return compiled;
47
+ }
48
+
49
+ function escapeRe(s: string): string {
50
+ return s.replace(/[.+^${}()|[\]\\*?]/g, '\\$&');
51
+ }
52
+
53
+ export function matchGlob(path: string, glob: string): boolean {
54
+ return globToRegExp(glob).test(path);
55
+ }
56
+
57
+ /** The partition a root-relative path belongs to: the first matching rule, in registry order. */
58
+ export function partitionOf(path: string, reg: PartitionRegistry): string | undefined {
59
+ for (const rule of reg.partitions) {
60
+ if (!rule.match.some((g) => matchGlob(path, g))) continue;
61
+ if (rule.id !== TOP_LEVEL_FOLDER) return rule.id;
62
+ const slash = path.indexOf('/');
63
+ return slash > 0 ? path.slice(0, slash) : undefined;
64
+ }
65
+ return undefined;
66
+ }
67
+
68
+ /** The order of a partition (folder partitions from {@link TOP_LEVEL_FOLDER} share that rule's). */
69
+ export function partitionOrder(id: string, reg: PartitionRegistry): number | undefined {
70
+ const rule = reg.partitions.find((r) => r.id === id) ?? reg.partitions.find((r) => r.id === TOP_LEVEL_FOLDER);
71
+ return rule?.order;
72
+ }
73
+
74
+ export function verdictOf(from: string | undefined, to: string | undefined, reg: PartitionRegistry): EdgeVerdict | undefined {
75
+ if (!reg.allow || from === undefined || to === undefined) return undefined;
76
+ if (from === to) return 'same';
77
+ if (reg.allow[from]?.includes(to)) return 'allowed';
78
+ const fromOrder = partitionOrder(from, reg);
79
+ const toOrder = partitionOrder(to, reg);
80
+ if (fromOrder !== undefined && toOrder !== undefined && toOrder < fromOrder) return 'upward';
81
+ return 'skips';
82
+ }
83
+
84
+ /**
85
+ * A pack's registry, or a validated `.builderforce/partitions.json` override. Throws a
86
+ * `TypeError` naming the first problem when the override is not a {@link PartitionRegistry}.
87
+ */
88
+ export function loadPartitions(pack: KnowledgePack, override?: unknown): PartitionRegistry {
89
+ if (override === undefined || override === null) return pack.partitions ?? { partitions: [] };
90
+ const fail = (why: string): never => { throw new TypeError(`partitions.json: ${why}`); };
91
+ if (typeof override !== 'object' || Array.isArray(override)) fail('expected an object');
92
+ const o = override as Record<string, unknown>;
93
+ if (!Array.isArray(o['partitions'])) fail('`partitions` must be an array');
94
+ const partitions: PartitionRule[] = (o['partitions'] as unknown[]).map((raw, i) => {
95
+ const r = raw as Record<string, unknown> | null;
96
+ if (!r || typeof r !== 'object') fail(`partitions[${i}] must be an object`);
97
+ if (typeof r!['id'] !== 'string' || !r!['id']) fail(`partitions[${i}].id must be a non-empty string`);
98
+ if (typeof r!['order'] !== 'number' || !Number.isFinite(r!['order'])) fail(`partitions[${i}].order must be a number`);
99
+ const match = r!['match'];
100
+ if (!Array.isArray(match) || !match.every((g) => typeof g === 'string' && g)) {
101
+ fail(`partitions[${i}].match must be an array of globs`);
102
+ }
103
+ return { id: r!['id'] as string, order: r!['order'] as number, match: [...(match as string[])] };
104
+ });
105
+ const ids = new Set(partitions.map((p) => p.id));
106
+ if (ids.size !== partitions.length) fail('partition ids must be unique');
107
+ if (o['allow'] === undefined) return { partitions };
108
+ const allowRaw = o['allow'];
109
+ if (!allowRaw || typeof allowRaw !== 'object' || Array.isArray(allowRaw)) fail('`allow` must be an object');
110
+ const allow: Record<string, string[]> = {};
111
+ for (const [from, to] of Object.entries(allowRaw as Record<string, unknown>)) {
112
+ if (!ids.has(from)) fail(`allow.${from} names an unknown partition`);
113
+ if (!Array.isArray(to) || !to.every((t) => typeof t === 'string' && ids.has(t))) {
114
+ fail(`allow.${from} must list known partition ids`);
115
+ }
116
+ allow[from] = [...(to as string[])];
117
+ }
118
+ return { partitions, allow };
119
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * knowledge/plan.ts — `planReview`: the confirm chip's numbers (PRD 38 §4.3).
3
+ *
4
+ * A walk plus a manifest diff by hash, nothing more: a file whose hash is unchanged is
5
+ * never read. The only reads are the ignore files (`.gitignore`, `.builderforce/ignore`)
6
+ * when the listing has them. Binary files (by extension), oversized files and files no
7
+ * pack accepts are skipped with a reason.
8
+ */
9
+
10
+ import type { IngestManifest } from '../ingest/types.js';
11
+ import type { DirectorySource, KnowledgePack, ReviewPlan } from './model.js';
12
+ import {
13
+ composePacks, DEFAULT_MAX_FILE_BYTES, filesManifestKey, IGNORE_FILES, ignoreMatcher, isBinaryPath,
14
+ manifestMap, stubSource,
15
+ } from './sources.js';
16
+ import { compareStr, sortBy } from './refs.js';
17
+
18
+ /** ≈ bytes per judged section, for the estimate (the plan does not read changed files). */
19
+ export const AVG_SECTION_BYTES = 1500;
20
+ /** ≈ one compare call per this many judged sections when contradictions are asked for. */
21
+ const SECTIONS_PER_COMPARE = 10;
22
+
23
+ export interface PlanOptions {
24
+ /** Per-file size cap in bytes. Default 2 MB. */
25
+ maxFileBytes?: number;
26
+ }
27
+
28
+ export interface PlannedFile {
29
+ path: string;
30
+ hash: string;
31
+ size: number;
32
+ pack: string;
33
+ changed: boolean;
34
+ }
35
+
36
+ export interface PlanDetail {
37
+ plan: ReviewPlan;
38
+ /** Every included file (changed and unchanged), sorted by path. */
39
+ files: PlannedFile[];
40
+ /** The manifest's prior `path → hash`. */
41
+ prior: Map<string, string>;
42
+ }
43
+
44
+ export async function planReview(
45
+ source: DirectorySource,
46
+ manifest: IngestManifest,
47
+ packs: KnowledgePack[],
48
+ opts: PlanOptions = {},
49
+ ): Promise<ReviewPlan> {
50
+ return (await planDetail(source, manifest, packs, opts)).plan;
51
+ }
52
+
53
+ export async function planDetail(
54
+ source: DirectorySource,
55
+ manifest: IngestManifest,
56
+ packs: readonly KnowledgePack[],
57
+ opts: PlanOptions = {},
58
+ ): Promise<PlanDetail> {
59
+ const maxBytes = opts.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
60
+ const listed: Array<{ path: string; hash: string; size: number }> = [];
61
+ for await (const entry of source.list()) listed.push({ ...entry, path: entry.path.replace(/\\/g, '/').replace(/^\.?\//, '') });
62
+ const entries = sortBy(listed, (e) => e.path);
63
+
64
+ const ignoreTexts: string[] = [];
65
+ for (const name of IGNORE_FILES) {
66
+ if (!entries.some((e) => e.path === name)) continue;
67
+ const doc = await source.read(name);
68
+ if (doc.content.kind === 'text') ignoreTexts.push(doc.content.text);
69
+ }
70
+ const ignored = ignoreMatcher(ignoreTexts);
71
+ const composed = composePacks(packs);
72
+ const prior = await manifestMap(manifest, filesManifestKey(source.rootId));
73
+ const packById = new Map(packs.map((p) => [p.id, p] as const));
74
+
75
+ const files: PlannedFile[] = [];
76
+ const skipped: ReviewPlan['skipped'] = [];
77
+ const byPack: Record<string, number> = {};
78
+ let estimatedSections = 0;
79
+ let compareSections = 0;
80
+ for (const e of entries) {
81
+ if (ignored(e.path)) { skipped.push({ path: e.path, reason: 'ignored' }); continue; }
82
+ if (isBinaryPath(e.path)) { skipped.push({ path: e.path, reason: 'binary' }); continue; }
83
+ if (e.size > maxBytes) { skipped.push({ path: e.path, reason: 'too-large' }); continue; }
84
+ const hit = composed.resolve(stubSource(e.path));
85
+ if (!hit) { skipped.push({ path: e.path, reason: 'no-pack' }); continue; }
86
+ const changed = prior.get(e.path) !== e.hash;
87
+ files.push({ path: e.path, hash: e.hash, size: e.size, pack: hit.pack.id, changed });
88
+ byPack[hit.pack.id] = (byPack[hit.pack.id] ?? 0) + 1;
89
+ const spec = packById.get(hit.pack.id)?.judgement;
90
+ if (changed && spec && (spec.claims || spec.entities)) {
91
+ const sections = Math.max(1, Math.ceil(e.size / AVG_SECTION_BYTES));
92
+ estimatedSections += sections;
93
+ if (spec.contradictions) compareSections += sections;
94
+ }
95
+ }
96
+
97
+ const present = new Set(entries.map((e) => e.path));
98
+ const removed = [...prior.keys()].filter((p) => !present.has(p)).sort(compareStr);
99
+ const changed = files.filter((f) => f.changed).map((f) => f.path);
100
+ const sortedByPack = Object.fromEntries(Object.keys(byPack).sort(compareStr).map((k) => [k, byPack[k]!]));
101
+ return {
102
+ plan: {
103
+ rootId: source.rootId,
104
+ files: files.length,
105
+ byPack: sortedByPack,
106
+ unchanged: files.length - changed.length,
107
+ changed,
108
+ removed,
109
+ skipped,
110
+ estimatedModelCalls: estimatedSections + Math.ceil(compareSections / SECTIONS_PER_COMPARE),
111
+ },
112
+ files,
113
+ prior,
114
+ };
115
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * knowledge/refs.ts — the NodeRef grammar and the deterministic primitives every
3
+ * knowledge module shares (ordering, canonical JSON, entity keys).
4
+ *
5
+ * Pure and import-free of `ingest` runtime code, so ingest's extractors can use it
6
+ * without an import cycle.
7
+ *
8
+ * src:${rootId}::${path} a source file (or an area: a directory path)
9
+ * src:${rootId}::${path}#${slug} a section of a source
10
+ * ent:${pack}:${normalizedName} an entity (also its project_facts key)
11
+ * fact:${key} any other fact (claims: fact:claim:${hash})
12
+ */
13
+
14
+ import type { NodeRef } from './model.js';
15
+
16
+ /** Code-unit ordering — never `localeCompare`, whose result depends on the host's ICU. */
17
+ export function compareStr(a: string, b: string): number {
18
+ return a < b ? -1 : a > b ? 1 : 0;
19
+ }
20
+
21
+ export function srcRef(rootId: string, path: string): NodeRef {
22
+ return `src:${rootId}::${path}`;
23
+ }
24
+
25
+ /** The root-relative path of a `src:` ref (the section suffix dropped), else undefined. */
26
+ export function pathOfRef(ref: string): string | undefined {
27
+ if (!ref.startsWith('src:')) return undefined;
28
+ const at = ref.indexOf('::');
29
+ if (at < 0) return undefined;
30
+ const rest = ref.slice(at + 2);
31
+ const hash = rest.indexOf('#');
32
+ return hash < 0 ? rest : rest.slice(0, hash);
33
+ }
34
+
35
+ /** The ref of the file a `src:` ref belongs to (itself for a file). */
36
+ export function fileRefOf(ref: NodeRef): NodeRef {
37
+ const hash = ref.startsWith('src:') ? ref.indexOf('#', ref.indexOf('::')) : -1;
38
+ return hash < 0 ? ref : (ref.slice(0, hash) as NodeRef);
39
+ }
40
+
41
+ /** The node ref of a stored fact key: `ent:` keys are refs already; the rest are `fact:`. */
42
+ export function factRef(key: string): NodeRef {
43
+ return key.startsWith('ent:') ? (key as NodeRef) : `fact:${key}`;
44
+ }
45
+
46
+ /** The stored fact key of a fact/entity ref (inverse of {@link factRef}). */
47
+ export function factKeyOf(ref: NodeRef): string | undefined {
48
+ if (ref.startsWith('ent:')) return ref;
49
+ if (ref.startsWith('fact:')) return ref.slice(5);
50
+ return undefined;
51
+ }
52
+
53
+ /**
54
+ * The normalized key of a name: NFKC, case-folded, punctuation stripped (dashes,
55
+ * underscores and slashes read as spaces so `run-store` and `run store` agree),
56
+ * whitespace collapsed.
57
+ */
58
+ export function normalizeKey(name: string): string {
59
+ return name
60
+ .normalize('NFKC')
61
+ .toLowerCase()
62
+ .replace(/[\p{Pd}\p{Pc}/\\]+/gu, ' ')
63
+ .replace(/[\p{P}\p{S}]+/gu, '')
64
+ .replace(/\s+/g, ' ')
65
+ .trim();
66
+ }
67
+
68
+ /** A heading slug: the normalized key with spaces as dashes. */
69
+ export function slugOf(text: string): string {
70
+ return normalizeKey(text).replace(/ /g, '-') || 'section';
71
+ }
72
+
73
+ export function basenameOf(path: string): string {
74
+ const i = path.lastIndexOf('/');
75
+ return i < 0 ? path : path.slice(i + 1);
76
+ }
77
+
78
+ /** Rounds to 6 decimals so floating noise never reaches the JSON. */
79
+ export function round6(n: number): number {
80
+ return Math.round(n * 1e6) / 1e6;
81
+ }
82
+
83
+ /** JSON with object keys sorted and `undefined` members dropped — the canonical form for digests. */
84
+ export function canonicalJson(value: unknown): string {
85
+ return JSON.stringify(sortKeys(value));
86
+ }
87
+
88
+ function sortKeys(value: unknown): unknown {
89
+ if (Array.isArray(value)) return value.map(sortKeys);
90
+ if (value === null || typeof value !== 'object') return value;
91
+ const out: Record<string, unknown> = {};
92
+ for (const key of Object.keys(value as Record<string, unknown>).sort(compareStr)) {
93
+ const v = (value as Record<string, unknown>)[key];
94
+ if (v !== undefined) out[key] = sortKeys(v);
95
+ }
96
+ return out;
97
+ }
98
+
99
+ /** Sorts a copy by the given string keys, in order. */
100
+ export function sortBy<T>(items: readonly T[], ...keys: Array<(t: T) => string>): T[] {
101
+ return [...items].sort((a, b) => {
102
+ for (const key of keys) {
103
+ const c = compareStr(key(a), key(b));
104
+ if (c !== 0) return c;
105
+ }
106
+ return 0;
107
+ });
108
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * knowledge/resolve.ts — deterministic entity resolution (PRD 38 §4.3).
3
+ *
4
+ * • Exact: names whose normalized keys ({@link normalizeKey}) are equal are ONE entity.
5
+ * • Near: pairs the knowledge index calls similar become `possible-same` findings for a
6
+ * person to judge. They are never merged automatically.
7
+ */
8
+
9
+ import type { Finding, NodeRef } from './model.js';
10
+ import type { SimilarPair } from '../retrieval/knowledge-index/types.js';
11
+ import { hashText } from '../ingest/hash.js';
12
+ import { compareStr, normalizeKey, round6 } from './refs.js';
13
+
14
+ export { normalizeKey };
15
+
16
+ export interface ResolvedEntity {
17
+ key: string;
18
+ /** The canonical ref (the smallest of the group). */
19
+ ref: NodeRef;
20
+ /** Every ref with this key, sorted. */
21
+ refs: NodeRef[];
22
+ }
23
+
24
+ /** Groups refs by exact normalized key. Empty keys are ignored. Sorted by key. */
25
+ export function resolveEntities(entries: ReadonlyArray<{ ref: NodeRef; key: string }>): ResolvedEntity[] {
26
+ const groups = new Map<string, Set<NodeRef>>();
27
+ for (const { ref, key } of entries) {
28
+ const k = normalizeKey(key);
29
+ if (!k) continue;
30
+ let g = groups.get(k);
31
+ if (!g) groups.set(k, (g = new Set()));
32
+ g.add(ref);
33
+ }
34
+ return [...groups.keys()].sort(compareStr).map((key) => {
35
+ const refs = [...groups.get(key)!].sort(compareStr);
36
+ return { key, ref: refs[0]!, refs };
37
+ });
38
+ }
39
+
40
+ /** Normalized key → canonical ref, for an extractor's `ctx.knownKeys`. */
41
+ export function knownKeysOf(resolved: readonly ResolvedEntity[]): Map<string, NodeRef> {
42
+ return new Map(resolved.map((r) => [r.key, r.ref] as const));
43
+ }
44
+
45
+ /**
46
+ * `possible-same` findings from near-duplicate pairs between two DIFFERENT entities (pairs
47
+ * already merged by key, or naming a non-entity, are not reported).
48
+ */
49
+ export function possibleSameFindings(pairs: readonly SimilarPair[], pack = 'memory'): Finding[] {
50
+ const seen = new Set<string>();
51
+ const out: Finding[] = [];
52
+ for (const p of pairs) {
53
+ if (!p.a.startsWith('ent:') || !p.b.startsWith('ent:') || p.a === p.b) continue;
54
+ const [a, b] = compareStr(p.a, p.b) <= 0 ? [p.a, p.b] : [p.b, p.a];
55
+ if (normalizeKey(entityName(a)) === normalizeKey(entityName(b))) continue;
56
+ const id = `possible-same:${hashText(`${a}\u0000${b}`)}`;
57
+ if (seen.has(id)) continue;
58
+ seen.add(id);
59
+ out.push({
60
+ id, kind: 'possible-same', pack, nodes: [a as NodeRef, b as NodeRef], severity: 'info',
61
+ params: { similarity: round6(p.similarity) },
62
+ requestKey: 'knowledgeGraph.fix.possibleSame',
63
+ });
64
+ }
65
+ return out.sort((x, y) => compareStr(x.id, y.id));
66
+ }
67
+
68
+ /** The name part of `ent:<pack>:<name>`. */
69
+ export function entityName(ref: string): string {
70
+ const rest = ref.startsWith('ent:') ? ref.slice(4) : ref;
71
+ const colon = rest.indexOf(':');
72
+ return colon < 0 ? rest : rest.slice(colon + 1);
73
+ }
74
+
75
+ /** The stored key of an entity named `name` in `pack`. */
76
+ export function entityKey(pack: string, name: string): NodeRef {
77
+ return `ent:${pack}:${normalizeKey(name)}`;
78
+ }