@fortemi/core 2026.7.0 → 2026.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.
package/README.md CHANGED
@@ -126,10 +126,11 @@ Use this subpath for Pagenary-style command palettes, static docs search, and
126
126
  vanilla JavaScript review surfaces. The top-level `@fortemi/core` export remains
127
127
  available for full archive/runtime integrations.
128
128
 
129
- The record contract keeps the existing flat fields for filtering and search:
130
- `facets`, `tags`, `concepts`, `relationships`, and `provenance`. Static
131
- consumers that render metadata tabs can also read optional rich fields from full
132
- records:
129
+ The AIWG index adapter accepts both `aiwg.fortemi.index.export.v1` and
130
+ `aiwg.fortemi.index.export.v2` envelopes. The v1 record contract keeps the
131
+ existing flat fields for filtering and search: `facets`, `tags`, `concepts`,
132
+ `relationships`, and `provenance`. Static consumers that render metadata tabs
133
+ can also read optional rich fields from full records:
133
134
 
134
135
  - `skos_concepts`: concept ids plus labels, definitions, schemes, notation, URIs,
135
136
  alternate labels, and metadata.
@@ -140,10 +141,14 @@ records:
140
141
  - `relationships[*].metadata`: optional relationship labels, confidence,
141
142
  privacy, and structured metadata.
142
143
 
143
- These fields are additive to `aiwg.fortemi.index.record.v1`. Existing consumers
144
- can ignore them and continue querying the flat fields. Chunked indexes with a
145
- projection keep rich metadata in detail records; call `getRecord(id)` before
146
- rendering a metadata panel.
144
+ These fields are additive to `aiwg.fortemi.index.record.v1`. Version 2 records
145
+ can also carry AIWG migration fields such as `search`, `chunks`, `embeddings`,
146
+ `source.origin`, `source.generated`, `source.checksum`, `source.updated_at`,
147
+ `privacy.locality`, and compatibility metadata. Query helpers use v2 `search`
148
+ and `chunks` text when the old top-level `title`/`text` fields are absent.
149
+ Existing consumers can ignore the v2 fields and continue querying the flat
150
+ fields. Chunked indexes with a projection keep rich metadata in detail records;
151
+ call `getRecord(id)` before rendering a metadata panel.
147
152
 
148
153
  Large static indexes can use the chunked browser path instead of downloading one
149
154
  full `aiwg.fortemi.index.export.v1` file. Host a manifest plus deterministic part
@@ -179,9 +184,11 @@ results, but the controller keeps only a bounded part cache and never sets a
179
184
  materialized full export in `getIndex()`.
180
185
 
181
186
  AIWG records can use project-specific string types such as `aiwg.skill`,
182
- `aiwg.command`, `aiwg.rule`, `aiwg.requirement`, and `research.ref`. Validation
183
- still enforces required record fields, but it does not require unknown AIWG,
184
- research, or documentation domains to collapse into `aiwg.artifact`.
187
+ `aiwg.agent`, `aiwg.command`, `aiwg.rule`, `aiwg.flow`, `aiwg.bundle`,
188
+ `aiwg.kb.page`, `aiwg.memory.entry`, `aiwg.issue`, `research.ref`, and
189
+ `aiwg.research.profile`. Validation still enforces required record fields, but
190
+ it does not require unknown AIWG, research, KB, memory, issue, or documentation
191
+ domains to collapse into `aiwg.artifact`.
185
192
 
186
193
  For AIWG command-palette parity, opt into discovery ranking instead of the
187
194
  default deterministic substring lookup:
@@ -203,14 +210,19 @@ Chunked indexes also support graph navigation without loading a full export:
203
210
  const neighbors = await controller.neighbors('aiwg:requirement:search', {
204
211
  direction: 'both',
205
212
  relationshipType: 'cites',
213
+ relationshipDirection: 'downstream',
206
214
  })
207
215
 
208
216
  const graph = await controller.toCommunityGraphChunked()
209
217
  ```
210
218
 
211
- When scan parts are projected, relationship traversal scans the parts and
212
- lazy-loads detail records only when relationships are not present in the scan
213
- projection.
219
+ Relationship edges preserve v2 `source_path`, `target_path`, and
220
+ `direction: 'upstream' | 'downstream' | 'related'` values. Use
221
+ `direction: 'in' | 'out' | 'both'` for graph orientation around a node and
222
+ `relationshipDirection` for AIWG dependency/citation/profile/KB direction
223
+ semantics. When scan parts are projected, relationship traversal scans the parts
224
+ and lazy-loads detail records only when relationships are not present in the
225
+ scan projection.
214
226
 
215
227
  Static semantic search uses an optional sidecar contract:
216
228
 
@@ -281,11 +293,11 @@ pnpm add @fortemi/react @fortemi/core react
281
293
 
282
294
  ## Documentation
283
295
 
284
- - [Repository API](https://github.com/Fortemi/fortemi-react/blob/main/docs/api-reference.md#repositories)
285
- - [Integration guide](https://github.com/Fortemi/fortemi-react/blob/main/docs/integration.md)
286
- - [Search guide](https://github.com/Fortemi/fortemi-react/blob/main/docs/search.md)
287
- - [Extending Fortemi](https://github.com/Fortemi/fortemi-react/blob/main/docs/extending.md)
288
- - [Supply-chain controls](https://github.com/Fortemi/fortemi-react/blob/main/docs/security/supply-chain.md)
296
+ - [Repository API](https://github.com/Fortemi/fortemi-react/blob/main/docs/content/api-reference.md#repositories)
297
+ - [Integration guide](https://github.com/Fortemi/fortemi-react/blob/main/docs/content/guides/integration.md)
298
+ - [Search guide](https://github.com/Fortemi/fortemi-react/blob/main/docs/content/guides/search.md)
299
+ - [Extending Fortemi](https://github.com/Fortemi/fortemi-react/blob/main/docs/content/advanced/extending.md)
300
+ - [Supply-chain controls](https://github.com/Fortemi/fortemi-react/blob/main/docs/content/security/supply-chain.md)
289
301
 
290
302
  ## License
291
303
 
@@ -1,17 +1,26 @@
1
1
  type AiwgFortemiKnownRecordType = 'crm.contact' | 'crm.organization' | 'crm.event' | 'crm.interaction' | 'aiwg.artifact' | 'docs.page';
2
2
  type AiwgFortemiRecordType = AiwgFortemiKnownRecordType | `aiwg.${string}` | `research.${string}` | `docs.${string}` | string;
3
+ type AiwgFortemiRecordSchemaVersion = 'aiwg.fortemi.index.record.v1' | 'aiwg.fortemi.index.record.v2';
4
+ type AiwgFortemiIndexExportSchemaVersion = 'aiwg.fortemi.index.export.v1' | 'aiwg.fortemi.index.export.v2';
3
5
  type AiwgPrivacyClassification = 'private' | 'sanitized' | 'public';
4
6
  type AiwgProvenanceConfidence = 'source' | 'candidate' | 'reviewed' | 'rejected';
5
7
  type AiwgReviewAction = 'accept' | 'reject' | 'defer';
8
+ type AiwgFortemiRelationshipDirection = 'upstream' | 'downstream' | 'related';
6
9
  interface AiwgFortemiRecordSource {
7
10
  path: string;
8
11
  repo_relative_path: string;
9
12
  locator: string;
13
+ origin?: string;
14
+ generated?: boolean;
15
+ checksum?: string;
16
+ updated_at?: string;
10
17
  }
11
18
  interface AiwgFortemiRelationship {
12
19
  type: string;
13
20
  target_id: string;
14
21
  source_path?: string;
22
+ target_path?: string;
23
+ direction?: AiwgFortemiRelationshipDirection;
15
24
  label?: string;
16
25
  confidence?: number;
17
26
  privacy?: AiwgPrivacyClassification;
@@ -54,18 +63,53 @@ interface AiwgFortemiProvenanceEvent {
54
63
  privacy?: AiwgPrivacyClassification;
55
64
  attributes?: Record<string, unknown>;
56
65
  }
66
+ interface AiwgFortemiSearchProjection {
67
+ title?: string;
68
+ name?: string;
69
+ summary?: string;
70
+ body?: string;
71
+ triggers?: string[];
72
+ aliases?: string[];
73
+ capability?: string;
74
+ tags?: string[];
75
+ phase?: string;
76
+ type?: string;
77
+ frontmatter?: Record<string, unknown>;
78
+ }
79
+ interface AiwgFortemiChunk {
80
+ id?: string;
81
+ text?: string;
82
+ body?: string;
83
+ summary?: string;
84
+ source_path?: string;
85
+ metadata?: Record<string, unknown>;
86
+ }
87
+ interface AiwgFortemiRecordEmbedding {
88
+ id?: string;
89
+ embedding?: number[];
90
+ vector?: number[];
91
+ model?: string;
92
+ granularity?: string;
93
+ input_hash?: string;
94
+ source_path?: string;
95
+ metadata?: Record<string, unknown>;
96
+ }
57
97
  interface AiwgFortemiRecord {
58
- schema_version: 'aiwg.fortemi.index.record.v1';
98
+ schema_version: AiwgFortemiRecordSchemaVersion;
59
99
  id: string;
60
100
  type: AiwgFortemiRecordType;
61
101
  source: AiwgFortemiRecordSource;
62
- title: string;
63
- text: string;
102
+ title?: string;
103
+ text?: string;
64
104
  facets: Record<string, string[]>;
65
105
  tags: string[];
66
106
  concepts: string[];
67
107
  relationships: AiwgFortemiRelationship[];
68
108
  provenance: AiwgFortemiProvenance[];
109
+ search?: AiwgFortemiSearchProjection;
110
+ chunks?: AiwgFortemiChunk[];
111
+ embeddings?: AiwgFortemiRecordEmbedding[];
112
+ compatibility?: Record<string, unknown>;
69
113
  /** Optional rich SKOS metadata for static consumers that need labels/definitions without opening a shard. */
70
114
  skos_concepts?: AiwgFortemiSkosConcept[];
71
115
  /** Optional SKOS relationship edges among concepts referenced by this record. */
@@ -75,17 +119,23 @@ interface AiwgFortemiRecord {
75
119
  privacy: {
76
120
  classification: AiwgPrivacyClassification;
77
121
  pii: boolean;
122
+ locality?: string;
78
123
  };
79
124
  updated_at: string;
80
125
  }
81
126
  interface AiwgFortemiIndexExport {
82
- schema_version: 'aiwg.fortemi.index.export.v1';
127
+ schema_version: AiwgFortemiIndexExportSchemaVersion;
83
128
  generated_at: string;
84
129
  source: {
85
130
  repo: string;
86
131
  privacy: AiwgPrivacyClassification;
132
+ origin?: string;
133
+ generated?: boolean;
134
+ checksum?: string;
135
+ updated_at?: string;
87
136
  };
88
137
  items: AiwgFortemiRecord[];
138
+ compatibility?: Record<string, unknown>;
89
139
  }
90
140
  interface AiwgFortemiChunkPartRef {
91
141
  href: string;
@@ -215,6 +265,7 @@ type AiwgRelationshipSetOperation = 'intersection' | 'union' | 'difference';
215
265
  interface AiwgRelationshipTraversalOptions {
216
266
  direction?: AiwgRelationshipDirection;
217
267
  relationshipType?: string;
268
+ relationshipDirection?: AiwgFortemiRelationshipDirection;
218
269
  limit?: number;
219
270
  }
220
271
  interface AiwgRelationshipQueryOptions extends AiwgRelationshipTraversalOptions {
@@ -227,6 +278,8 @@ interface AiwgRelationshipEdgeSummary {
227
278
  target_id: string;
228
279
  type: string;
229
280
  source_path?: string;
281
+ target_path?: string;
282
+ direction?: AiwgFortemiRelationshipDirection;
230
283
  }
231
284
  interface AiwgRelationshipNodeSummary {
232
285
  id: string;
@@ -381,4 +434,4 @@ declare function aiwgFortemiIndexToCommunityGraph(index: AiwgFortemiIndexExport,
381
434
  }[];
382
435
  };
383
436
 
384
- export { AIWG_SCAN_REQUIRED_FIELDS, type AiwgChunkedIndexBuildOptions, type AiwgChunkedIndexBuildResult, type AiwgChunkedIndexDetailLoader, type AiwgChunkedIndexLoadOptions, type AiwgChunkedIndexLoader, type AiwgChunkedIndexProgress, type AiwgChunkedIndexProgressPhase, type AiwgChunkedIndexQueryOptions, type AiwgChunkedIndexQueryResult, type AiwgChunkedIndexValidationResult, type AiwgDetailIdEncoding, type AiwgFortemiChunkDetailRef, type AiwgFortemiChunkManifest, type AiwgFortemiChunkPart, type AiwgFortemiChunkPartRef, type AiwgFortemiIndexExport, type AiwgFortemiKnownRecordType, type AiwgFortemiProjectedRecord, type AiwgFortemiProvenance, type AiwgFortemiProvenanceEvent, type AiwgFortemiRecord, type AiwgFortemiRecordSource, type AiwgFortemiRecordType, type AiwgFortemiRelationship, type AiwgFortemiSkosConcept, type AiwgFortemiSkosRelation, type AiwgFortemiSkosRelationType, type AiwgIndexController, type AiwgIndexControllerListener, type AiwgIndexControllerSnapshot, type AiwgIndexGraphOptions, type AiwgIndexQueryMatch, type AiwgIndexQueryOptions, type AiwgIndexQueryRankedItem, type AiwgIndexQueryResult, type AiwgIndexQueryWeights, type AiwgIndexValidationResult, type AiwgPrivacyClassification, type AiwgProvenanceConfidence, type AiwgRelationshipDirection, type AiwgRelationshipEdgeSummary, type AiwgRelationshipNodeSummary, type AiwgRelationshipQueryOptions, type AiwgRelationshipSetOperation, type AiwgRelationshipSetOptions, type AiwgRelationshipSetResult, type AiwgRelationshipTraversalOptions, type AiwgRelationshipTraversalResult, type AiwgReviewAction, type AiwgReviewDecision, type AiwgReviewDecisionExport, type AiwgReviewInput, type AiwgStaticDuplicatePair, type AiwgStaticEmbeddingRecord, type AiwgStaticEmbeddingSet, type AiwgStaticHybridQueryOptions, type AiwgStaticSemanticQueryOptions, type AiwgStaticSemanticResult, aiwgDetailHrefForId, aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, assertAiwgStaticEmbeddingSet, buildAiwgChunkedIndex, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, encodeAiwgDetailId, findAiwgStaticDuplicatePairs, getAiwgFortemiFacets, queryAiwgFortemiIndex, queryAiwgHybridIndex, queryAiwgSemanticIndex, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateAiwgStaticEmbeddingSet };
437
+ export { AIWG_SCAN_REQUIRED_FIELDS, type AiwgChunkedIndexBuildOptions, type AiwgChunkedIndexBuildResult, type AiwgChunkedIndexDetailLoader, type AiwgChunkedIndexLoadOptions, type AiwgChunkedIndexLoader, type AiwgChunkedIndexProgress, type AiwgChunkedIndexProgressPhase, type AiwgChunkedIndexQueryOptions, type AiwgChunkedIndexQueryResult, type AiwgChunkedIndexValidationResult, type AiwgDetailIdEncoding, type AiwgFortemiChunk, type AiwgFortemiChunkDetailRef, type AiwgFortemiChunkManifest, type AiwgFortemiChunkPart, type AiwgFortemiChunkPartRef, type AiwgFortemiIndexExport, type AiwgFortemiIndexExportSchemaVersion, type AiwgFortemiKnownRecordType, type AiwgFortemiProjectedRecord, type AiwgFortemiProvenance, type AiwgFortemiProvenanceEvent, type AiwgFortemiRecord, type AiwgFortemiRecordEmbedding, type AiwgFortemiRecordSchemaVersion, type AiwgFortemiRecordSource, type AiwgFortemiRecordType, type AiwgFortemiRelationship, type AiwgFortemiRelationshipDirection, type AiwgFortemiSearchProjection, type AiwgFortemiSkosConcept, type AiwgFortemiSkosRelation, type AiwgFortemiSkosRelationType, type AiwgIndexController, type AiwgIndexControllerListener, type AiwgIndexControllerSnapshot, type AiwgIndexGraphOptions, type AiwgIndexQueryMatch, type AiwgIndexQueryOptions, type AiwgIndexQueryRankedItem, type AiwgIndexQueryResult, type AiwgIndexQueryWeights, type AiwgIndexValidationResult, type AiwgPrivacyClassification, type AiwgProvenanceConfidence, type AiwgRelationshipDirection, type AiwgRelationshipEdgeSummary, type AiwgRelationshipNodeSummary, type AiwgRelationshipQueryOptions, type AiwgRelationshipSetOperation, type AiwgRelationshipSetOptions, type AiwgRelationshipSetResult, type AiwgRelationshipTraversalOptions, type AiwgRelationshipTraversalResult, type AiwgReviewAction, type AiwgReviewDecision, type AiwgReviewDecisionExport, type AiwgReviewInput, type AiwgStaticDuplicatePair, type AiwgStaticEmbeddingRecord, type AiwgStaticEmbeddingSet, type AiwgStaticHybridQueryOptions, type AiwgStaticSemanticQueryOptions, type AiwgStaticSemanticResult, aiwgDetailHrefForId, aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, assertAiwgStaticEmbeddingSet, buildAiwgChunkedIndex, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, encodeAiwgDetailId, findAiwgStaticDuplicatePairs, getAiwgFortemiFacets, queryAiwgFortemiIndex, queryAiwgHybridIndex, queryAiwgSemanticIndex, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateAiwgStaticEmbeddingSet };
@@ -15,8 +15,6 @@ var REQUIRED_RECORD_FIELDS = [
15
15
  "id",
16
16
  "type",
17
17
  "source",
18
- "title",
19
- "text",
20
18
  "facets",
21
19
  "tags",
22
20
  "concepts",
@@ -57,6 +55,12 @@ function isPlainRecord(value) {
57
55
  function isOptionalStringArray(value) {
58
56
  return value === void 0 || Array.isArray(value) && value.every((item) => typeof item === "string");
59
57
  }
58
+ function isSupportedIndexSchemaVersion(value) {
59
+ return value === "aiwg.fortemi.index.export.v1" || value === "aiwg.fortemi.index.export.v2";
60
+ }
61
+ function isSupportedRecordSchemaVersion(value) {
62
+ return value === "aiwg.fortemi.index.record.v1" || value === "aiwg.fortemi.index.record.v2";
63
+ }
60
64
  function validateOptionalRichMetadata(item, index, errors) {
61
65
  if (item.skos_concepts !== void 0) {
62
66
  if (!Array.isArray(item.skos_concepts)) {
@@ -103,28 +107,80 @@ function validateOptionalRichMetadata(item, index, errors) {
103
107
  if (relationship.metadata !== void 0 && !isPlainRecord(relationship.metadata)) {
104
108
  errors.push("items[" + index + "].relationships[" + relationshipIndex + "].metadata must be an object");
105
109
  }
110
+ if (relationship.direction !== void 0 && relationship.direction !== "upstream" && relationship.direction !== "downstream" && relationship.direction !== "related") {
111
+ errors.push("items[" + index + "].relationships[" + relationshipIndex + "].direction must be upstream, downstream, or related");
112
+ }
113
+ if (relationship.target_path !== void 0 && typeof relationship.target_path !== "string") {
114
+ errors.push("items[" + index + "].relationships[" + relationshipIndex + "].target_path must be a string");
115
+ }
116
+ }
117
+ }
118
+ if (item.search !== void 0) {
119
+ if (!isPlainRecord(item.search)) {
120
+ errors.push("items[" + index + "].search must be an object");
121
+ } else {
122
+ if (!isOptionalStringArray(item.search.triggers)) errors.push("items[" + index + "].search.triggers must be a string array");
123
+ if (!isOptionalStringArray(item.search.aliases)) errors.push("items[" + index + "].search.aliases must be a string array");
124
+ if (!isOptionalStringArray(item.search.tags)) errors.push("items[" + index + "].search.tags must be a string array");
125
+ if (item.search.frontmatter !== void 0 && !isPlainRecord(item.search.frontmatter)) {
126
+ errors.push("items[" + index + "].search.frontmatter must be an object");
127
+ }
128
+ }
129
+ }
130
+ if (item.chunks !== void 0) {
131
+ if (!Array.isArray(item.chunks)) {
132
+ errors.push("items[" + index + "].chunks must be an array when present");
133
+ } else {
134
+ for (const [chunkIndex, chunk] of item.chunks.entries()) {
135
+ if (!isPlainRecord(chunk)) errors.push("items[" + index + "].chunks[" + chunkIndex + "] must be an object");
136
+ if (chunk.metadata !== void 0 && !isPlainRecord(chunk.metadata)) {
137
+ errors.push("items[" + index + "].chunks[" + chunkIndex + "].metadata must be an object");
138
+ }
139
+ }
140
+ }
141
+ }
142
+ if (item.embeddings !== void 0) {
143
+ if (!Array.isArray(item.embeddings)) {
144
+ errors.push("items[" + index + "].embeddings must be an array when present");
145
+ } else {
146
+ for (const [embeddingIndex, embedding] of item.embeddings.entries()) {
147
+ if (!isPlainRecord(embedding)) errors.push("items[" + index + "].embeddings[" + embeddingIndex + "] must be an object");
148
+ const vector = embedding.embedding ?? embedding.vector;
149
+ if (vector !== void 0 && (!Array.isArray(vector) || !vector.every((entry) => typeof entry === "number"))) {
150
+ errors.push("items[" + index + "].embeddings[" + embeddingIndex + "].embedding/vector must be a number array");
151
+ }
152
+ if (embedding.metadata !== void 0 && !isPlainRecord(embedding.metadata)) {
153
+ errors.push("items[" + index + "].embeddings[" + embeddingIndex + "].metadata must be an object");
154
+ }
155
+ }
106
156
  }
107
157
  }
158
+ if (item.compatibility !== void 0 && !isPlainRecord(item.compatibility)) {
159
+ errors.push("items[" + index + "].compatibility must be an object");
160
+ }
108
161
  }
109
162
  function validateAiwgFortemiIndexExport(value) {
110
163
  const errors = [];
111
164
  const counts = {};
112
165
  const data = value;
113
- if (data?.schema_version !== "aiwg.fortemi.index.export.v1") {
114
- errors.push("schema_version must be aiwg.fortemi.index.export.v1");
166
+ if (!isSupportedIndexSchemaVersion(data?.schema_version)) {
167
+ errors.push("schema_version must be aiwg.fortemi.index.export.v1 or aiwg.fortemi.index.export.v2");
115
168
  }
116
169
  if (!hasString(data?.generated_at)) errors.push("generated_at is required");
117
170
  if (!hasString(data?.source?.repo)) errors.push("source.repo is required");
118
171
  if (!hasString(data?.source?.privacy)) errors.push("source.privacy is required");
119
172
  if (!Array.isArray(data?.items)) errors.push("items must be an array");
173
+ if (data.compatibility !== void 0 && !isPlainRecord(data.compatibility)) {
174
+ errors.push("compatibility must be an object");
175
+ }
120
176
  const ids = /* @__PURE__ */ new Set();
121
177
  let previousId = "";
122
178
  for (const [index, item] of (data.items ?? []).entries()) {
123
179
  for (const field of REQUIRED_RECORD_FIELDS) {
124
180
  if (!(field in item)) errors.push("items[" + index + "]." + field + " is required");
125
181
  }
126
- if (item.schema_version !== "aiwg.fortemi.index.record.v1") {
127
- errors.push("items[" + index + "].schema_version must be aiwg.fortemi.index.record.v1");
182
+ if (!isSupportedRecordSchemaVersion(item.schema_version)) {
183
+ errors.push("items[" + index + "].schema_version must be aiwg.fortemi.index.record.v1 or aiwg.fortemi.index.record.v2");
128
184
  }
129
185
  if (!hasString(item.id)) errors.push("items[" + index + "].id is required");
130
186
  if (hasString(item.id) && ids.has(item.id)) errors.push("duplicate id: " + item.id);
@@ -138,6 +194,12 @@ function validateAiwgFortemiIndexExport(value) {
138
194
  if (!hasString(item.source?.path)) errors.push("items[" + index + "].source.path is required");
139
195
  if (!hasString(item.source?.repo_relative_path)) errors.push("items[" + index + "].source.repo_relative_path is required");
140
196
  if (!hasString(item.source?.locator)) errors.push("items[" + index + "].source.locator is required");
197
+ if (typeof item.title !== "string" && typeof item.search?.title !== "string" && typeof item.search?.name !== "string") {
198
+ errors.push("items[" + index + "].title or search.title/search.name is required");
199
+ }
200
+ if (typeof item.text !== "string" && typeof item.search?.body !== "string" && typeof item.search?.summary !== "string") {
201
+ errors.push("items[" + index + "].text or search.body/search.summary is required");
202
+ }
141
203
  if (!Array.isArray(item.tags)) errors.push("items[" + index + "].tags must be an array");
142
204
  if (!Array.isArray(item.concepts)) errors.push("items[" + index + "].concepts must be an array");
143
205
  if (!Array.isArray(item.relationships)) errors.push("items[" + index + "].relationships must be an array");
@@ -218,8 +280,8 @@ function validateProjectedRecords(items) {
218
280
  const ids = /* @__PURE__ */ new Set();
219
281
  let previousId = "";
220
282
  for (const [index, item] of items.entries()) {
221
- if (item.schema_version !== "aiwg.fortemi.index.record.v1") {
222
- errors.push("items[" + index + "].schema_version must be aiwg.fortemi.index.record.v1");
283
+ if (!isSupportedRecordSchemaVersion(item.schema_version)) {
284
+ errors.push("items[" + index + "].schema_version must be aiwg.fortemi.index.record.v1 or aiwg.fortemi.index.record.v2");
223
285
  }
224
286
  if (!hasString(item.id)) errors.push("items[" + index + "].id is required");
225
287
  if (hasString(item.id) && ids.has(item.id)) errors.push("duplicate id: " + item.id);
@@ -229,8 +291,12 @@ function validateProjectedRecords(items) {
229
291
  }
230
292
  if (hasString(item.id)) previousId = item.id;
231
293
  if (!hasString(item.type)) errors.push("items[" + index + "].type must be a non-empty string");
232
- if (!hasString(item.title)) errors.push("items[" + index + "].title is required");
233
- if (typeof item.text !== "string") errors.push("items[" + index + "].text is required");
294
+ if (typeof item.title !== "string" && typeof item.search?.title !== "string" && typeof item.search?.name !== "string") {
295
+ errors.push("items[" + index + "].title or search.title/search.name is required");
296
+ }
297
+ if (typeof item.text !== "string" && typeof item.search?.body !== "string" && typeof item.search?.summary !== "string") {
298
+ errors.push("items[" + index + "].text or search.body/search.summary is required");
299
+ }
234
300
  if (!item.facets || typeof item.facets !== "object" || Array.isArray(item.facets)) {
235
301
  errors.push("items[" + index + "].facets must be an object");
236
302
  }
@@ -322,6 +388,38 @@ function getAiwgFortemiFacets(items) {
322
388
  }
323
389
  return result;
324
390
  }
391
+ function recordTitle(item) {
392
+ return item.title ?? item.search?.title ?? item.search?.name ?? item.id;
393
+ }
394
+ function recordText(item) {
395
+ return item.text ?? item.search?.body ?? item.search?.summary ?? item.chunks?.map((chunk) => chunk.text ?? chunk.body ?? chunk.summary ?? "").filter(Boolean).join("\n") ?? "";
396
+ }
397
+ function recordSearchValues(item) {
398
+ const search = item.search;
399
+ const values = [
400
+ item.id,
401
+ recordTitle(item),
402
+ recordText(item),
403
+ search?.title,
404
+ search?.name,
405
+ search?.summary,
406
+ search?.body,
407
+ search?.capability,
408
+ search?.phase,
409
+ search?.type,
410
+ ...search?.triggers ?? [],
411
+ ...search?.aliases ?? [],
412
+ ...search?.tags ?? [],
413
+ ...(item.chunks ?? []).flatMap((chunk) => [chunk.text, chunk.body, chunk.summary, chunk.source_path])
414
+ ];
415
+ if (search?.frontmatter) {
416
+ for (const value of Object.values(search.frontmatter)) {
417
+ if (typeof value === "string") values.push(value);
418
+ else if (Array.isArray(value)) values.push(...value.filter((entry) => typeof entry === "string"));
419
+ }
420
+ }
421
+ return values.filter((value) => typeof value === "string" && value.length > 0);
422
+ }
325
423
  function buildAiwgChunkedIndex(index, options = {}) {
326
424
  const partSize = hasPositiveInteger(options.partSize) ? options.partSize : 500;
327
425
  const projection = options.projection;
@@ -383,14 +481,21 @@ function matchesFacetFilters(item, filters) {
383
481
  function queryMatches(item, q) {
384
482
  if (!q) return [];
385
483
  const matches = [];
386
- if (item.title.toLowerCase().includes(q)) matches.push({ field: "title", value: item.title });
387
- if (item.text.toLowerCase().includes(q)) matches.push({ field: "text", value: item.text });
484
+ const title = recordTitle(item);
485
+ const text = recordText(item);
486
+ if (title.toLowerCase().includes(q)) matches.push({ field: "title", value: title });
487
+ if (text.toLowerCase().includes(q)) matches.push({ field: "text", value: text });
388
488
  for (const tag of item.tags) {
389
489
  if (tag.toLowerCase().includes(q)) matches.push({ field: "tag", value: tag });
390
490
  }
391
491
  for (const concept of item.concepts) {
392
492
  if (concept.toLowerCase().includes(q)) matches.push({ field: "concept", value: concept });
393
493
  }
494
+ for (const value of recordSearchValues(item)) {
495
+ if (value !== title && value !== text && value.toLowerCase().includes(q)) {
496
+ matches.push({ field: "text", value, score: DEFAULT_QUERY_WEIGHTS.text });
497
+ }
498
+ }
394
499
  return matches;
395
500
  }
396
501
  var DISCOVERY_STOPWORDS = /* @__PURE__ */ new Set([
@@ -446,17 +551,28 @@ function discoveryMatches(item, query) {
446
551
  const idParts = item.id.split(/[:/]/);
447
552
  const names = [
448
553
  item.id,
449
- item.title,
554
+ recordTitle(item),
555
+ item.search?.name,
556
+ item.search?.title,
557
+ ...item.search?.aliases ?? [],
450
558
  ...idParts,
451
559
  ...facetValues(item, ["name", "canonical_name", "command", "skill", "agent", "rule"])
452
- ].filter(Boolean);
453
- const triggers = facetValues(item, ["trigger", "triggers", "trigger_phrase", "trigger_phrases"]);
560
+ ].filter((value) => hasString(value));
561
+ const triggers = [
562
+ ...facetValues(item, ["trigger", "triggers", "trigger_phrase", "trigger_phrases"]),
563
+ ...item.search?.triggers ?? []
564
+ ];
454
565
  const capabilities = [
455
566
  ...facetValues(item, ["capability", "capabilities", "summary", "description"]),
567
+ item.search?.capability,
568
+ item.search?.summary,
569
+ item.search?.phase,
570
+ item.search?.type,
571
+ ...item.search?.tags ?? [],
456
572
  ...item.concepts,
457
573
  ...item.tags
458
- ];
459
- const sourceValues = [item.source?.path, item.source?.repo_relative_path, item.source?.locator].filter(Boolean);
574
+ ].filter((value) => hasString(value));
575
+ const sourceValues = [item.source?.path, item.source?.repo_relative_path, item.source?.locator].filter((value) => hasString(value));
460
576
  for (const name of names) {
461
577
  const canonicalName = canonicalDiscoveryName(name);
462
578
  if (!canonicalName) continue;
@@ -466,8 +582,9 @@ function discoveryMatches(item, query) {
466
582
  addDiscoveryMatch(matches, { field: "id", value: name, score: 48, reason: "near canonical name" });
467
583
  }
468
584
  }
469
- const titleOverlap = tokenOverlapScore(tokens, item.title);
470
- if (titleOverlap > 0) addDiscoveryMatch(matches, { field: "title", value: item.title, score: 18 * titleOverlap, reason: "title token overlap" });
585
+ const title = recordTitle(item);
586
+ const titleOverlap = tokenOverlapScore(tokens, title);
587
+ if (titleOverlap > 0) addDiscoveryMatch(matches, { field: "title", value: title, score: 18 * titleOverlap, reason: "title token overlap" });
471
588
  for (const trigger of triggers) {
472
589
  const overlap = tokenOverlapScore(tokens, trigger);
473
590
  if (overlap > 0) addDiscoveryMatch(matches, { field: "facet", value: trigger, score: 34 * overlap, reason: "trigger phrase" });
@@ -476,8 +593,9 @@ function discoveryMatches(item, query) {
476
593
  const overlap = tokenOverlapScore(tokens, capability);
477
594
  if (overlap > 0) addDiscoveryMatch(matches, { field: "concept", value: capability, score: 22 * overlap, reason: "capability overlap" });
478
595
  }
479
- const textOverlap = tokenOverlapScore(tokens, item.text);
480
- if (textOverlap > 0) addDiscoveryMatch(matches, { field: "text", value: item.text, score: 8 * textOverlap, reason: "body token overlap" });
596
+ const text = recordText(item);
597
+ const textOverlap = tokenOverlapScore(tokens, text);
598
+ if (textOverlap > 0) addDiscoveryMatch(matches, { field: "text", value: text, score: 8 * textOverlap, reason: "body token overlap" });
481
599
  for (const source of sourceValues) {
482
600
  const overlap = tokenOverlapScore(tokens, source);
483
601
  if (overlap > 0) addDiscoveryMatch(matches, { field: "source", value: source, score: 2 * overlap, reason: "path overlap" });
@@ -505,7 +623,7 @@ function createSnippet(item, matches, q, maxLength) {
505
623
  const textMatch = matches.find((match) => match.field === "text");
506
624
  const titleMatch = matches.find((match) => match.field === "title");
507
625
  const firstMatch = textMatch ?? titleMatch ?? matches[0];
508
- return clipSnippet(firstMatch?.value ?? item.text, q, maxLength);
626
+ return clipSnippet(firstMatch?.value ?? recordText(item), q, maxLength);
509
627
  }
510
628
  function createRankedEntries(items, q, options, ordinalBase = 0) {
511
629
  const weights = { ...DEFAULT_QUERY_WEIGHTS, ...options.weights };
@@ -767,13 +885,16 @@ function edgeFromRelationship(sourceId, relationship) {
767
885
  source_id: sourceId,
768
886
  target_id: relationship.target_id,
769
887
  type: relationship.type,
770
- ...relationship.source_path ? { source_path: relationship.source_path } : {}
888
+ ...relationship.source_path ? { source_path: relationship.source_path } : {},
889
+ ...relationship.target_path ? { target_path: relationship.target_path } : {},
890
+ ...relationship.direction ? { direction: relationship.direction } : {}
771
891
  };
772
892
  }
773
893
  function relationshipMatches(edge, options = {}) {
774
894
  const type = relationshipTypeFilter(options);
775
895
  const direction = options.direction ?? "both";
776
896
  if (type && edge.type !== type) return false;
897
+ if (options.relationshipDirection && edge.direction !== options.relationshipDirection) return false;
777
898
  if (options.sourceId && edge.source_id !== options.sourceId) return false;
778
899
  if (options.targetId && edge.target_id !== options.targetId) return false;
779
900
  if (direction === "out" && options.targetId && edge.target_id !== options.targetId) return false;
@@ -781,7 +902,7 @@ function relationshipMatches(edge, options = {}) {
781
902
  return true;
782
903
  }
783
904
  function nodeSummary(item) {
784
- return { id: item.id, type: item.type, title: item.title };
905
+ return { id: item.id, type: item.type, title: recordTitle(item) };
785
906
  }
786
907
  function addNode(nodes, item) {
787
908
  if (item) nodes.set(item.id, nodeSummary(item));