@equationalapplications/core-llm-wiki 5.3.0 → 5.4.0

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.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { __privateAdd, MetadataRepository, EmbeddingService, SearchService, JobManager, PromptService, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, WikiSourceRefHashCollision, generateId, BaseRepository, emptyManifest, resolveNodeType, WikiStrictOntologyViolation, validateInlineEdges, resolveEdgeDefinitions, normalizeTitleKey, WikiTransactionError, extractSqliteCode } from './chunk-KIJINNYU.mjs';
2
- export { DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, HEAL_BATCH_SIZE, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, WikiBusyError, WikiDuplicateHashError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, chunkText, configureRandomSource, parseEmbedding, safeSlice, validateManifest } from './chunk-KIJINNYU.mjs';
3
- import { appendRelatedSection, buildConceptDocument, buildLogMd, buildEntityIndexMd, buildRootIndexMd, isAllowedOkfPath, parseRootIndexMd, parseEntityIndexMd, parseConcept, splitRelatedSection, extractMarkdownLinks, parseLogMd, parseEventIdComment, appendEventIdComment } from '@equationalapplications/core-okf';
1
+ import { __privateAdd, MetadataRepository, EmbeddingService, SearchService, JobManager, PromptService, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, WikiSourceRefHashCollision, generateId, BaseRepository, emptyManifest, resolveNodeType, WikiStrictOntologyViolation, validateInlineEdges, resolveEdgeDefinitions, normalizeTitleKey, WikiTransactionError, extractSqliteCode } from './chunk-N2NXN6PO.mjs';
2
+ export { DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, HEAL_BATCH_SIZE, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, WikiBusyError, WikiDuplicateHashError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, chunkText, configureRandomSource, parseEmbedding, safeSlice, validateManifest } from './chunk-N2NXN6PO.mjs';
3
+ import { appendRelatedSection, buildConceptDocument, buildLogMd, buildEntityIndexMd, buildRootIndexMd, isAllowedOkfPath, parseRootIndexMd, parseEntityIndexMd, parseConcept, splitRelatedSection, extractMarkdownLinks, parseLogMd, parseEventIdComment, latestVerified, formatSourcesJson, appendEventIdComment, parseVerifiedFlexible, parseCitationsList, deriveTrustTier, isStaleAfter } from '@equationalapplications/core-okf';
4
4
 
5
5
  // src/db/schema.ts
6
6
  async function setupDatabase(db, prefix) {
@@ -24,7 +24,15 @@ async function setupDatabase(db, prefix) {
24
24
  embedding_blob BLOB,
25
25
  okf_type TEXT,
26
26
  ontology_checked_at INTEGER,
27
- heal_checked_at INTEGER
27
+ heal_checked_at INTEGER,
28
+ lifecycle_status TEXT NOT NULL DEFAULT 'stable',
29
+ stale_after INTEGER,
30
+ generated_by TEXT,
31
+ last_verified_at INTEGER,
32
+ last_verified_by TEXT,
33
+ okf_sources TEXT,
34
+ okf_verified TEXT,
35
+ okf_usage_window TEXT
28
36
  );
29
37
 
30
38
  CREATE INDEX IF NOT EXISTS ${prefix}entries_entity_idx ON ${prefix}entries(entity_id);
@@ -60,7 +68,15 @@ async function setupDatabase(db, prefix) {
60
68
  updated_at INTEGER NOT NULL,
61
69
  resolved_at INTEGER,
62
70
  deleted_at INTEGER,
63
- okf_type TEXT
71
+ okf_type TEXT,
72
+ lifecycle_status TEXT NOT NULL DEFAULT 'stable',
73
+ stale_after INTEGER,
74
+ generated_by TEXT,
75
+ last_verified_at INTEGER,
76
+ last_verified_by TEXT,
77
+ okf_sources TEXT,
78
+ okf_verified TEXT,
79
+ okf_usage_window TEXT
64
80
  );
65
81
 
66
82
  CREATE INDEX IF NOT EXISTS ${prefix}tasks_entity_idx ON ${prefix}tasks(entity_id, status);
@@ -122,6 +138,24 @@ async function setupDatabase(db, prefix) {
122
138
  CREATE INDEX IF NOT EXISTS ${prefix}outbox_created_at
123
139
  ON ${prefix}outbox (created_at);
124
140
  `);
141
+ await createOkfV02IndexesIfColumnsExist(db, prefix);
142
+ }
143
+ async function createOkfV02IndexesIfColumnsExist(db, prefix) {
144
+ for (const table of ["entries", "tasks"]) {
145
+ const cols = await db.getAllAsync(
146
+ `PRAGMA table_info(${prefix}${table})`
147
+ );
148
+ const colNames = new Set(cols.map((c) => c.name));
149
+ if (colNames.has("lifecycle_status")) {
150
+ await db.execAsync(`CREATE INDEX IF NOT EXISTS ${prefix}${table}_lifecycle_status_idx ON ${prefix}${table}(lifecycle_status);`);
151
+ }
152
+ if (colNames.has("stale_after")) {
153
+ await db.execAsync(`CREATE INDEX IF NOT EXISTS ${prefix}${table}_stale_after_idx ON ${prefix}${table}(stale_after);`);
154
+ }
155
+ if (colNames.has("last_verified_at")) {
156
+ await db.execAsync(`CREATE INDEX IF NOT EXISTS ${prefix}${table}_last_verified_at_idx ON ${prefix}${table}(last_verified_at);`);
157
+ }
158
+ }
125
159
  }
126
160
 
127
161
  // src/db/serializedAdapter.ts
@@ -351,6 +385,39 @@ var MIGRATIONS = [
351
385
  WHERE rn = 1;`
352
386
  );
353
387
  }
388
+ },
389
+ {
390
+ version: 10,
391
+ description: "OKF v0.2: add lifecycle_status, stale_after, generated_by, last_verified_*, okf_sources, okf_verified, okf_usage_window to entries and tasks",
392
+ run: async (db, prefix) => {
393
+ for (const table of ["entries", "tasks"]) {
394
+ const cols = await db.getAllAsync(
395
+ `PRAGMA table_info(${prefix}${table})`
396
+ );
397
+ const existing = new Set(cols.map((c) => c.name));
398
+ const adds = [
399
+ ["lifecycle_status", `ALTER TABLE ${prefix}${table} ADD COLUMN lifecycle_status TEXT NOT NULL DEFAULT 'stable'`],
400
+ ["stale_after", `ALTER TABLE ${prefix}${table} ADD COLUMN stale_after INTEGER`],
401
+ ["generated_by", `ALTER TABLE ${prefix}${table} ADD COLUMN generated_by TEXT`],
402
+ ["last_verified_at", `ALTER TABLE ${prefix}${table} ADD COLUMN last_verified_at INTEGER`],
403
+ ["last_verified_by", `ALTER TABLE ${prefix}${table} ADD COLUMN last_verified_by TEXT`],
404
+ ["okf_sources", `ALTER TABLE ${prefix}${table} ADD COLUMN okf_sources TEXT`],
405
+ ["okf_verified", `ALTER TABLE ${prefix}${table} ADD COLUMN okf_verified TEXT`],
406
+ ["okf_usage_window", `ALTER TABLE ${prefix}${table} ADD COLUMN okf_usage_window TEXT`]
407
+ ];
408
+ for (const [name, sql] of adds) {
409
+ if (!existing.has(name)) await db.execAsync(sql);
410
+ }
411
+ }
412
+ await db.execAsync(`
413
+ CREATE INDEX IF NOT EXISTS ${prefix}entries_lifecycle_status_idx ON ${prefix}entries(lifecycle_status);
414
+ CREATE INDEX IF NOT EXISTS ${prefix}entries_stale_after_idx ON ${prefix}entries(stale_after);
415
+ CREATE INDEX IF NOT EXISTS ${prefix}entries_last_verified_at_idx ON ${prefix}entries(last_verified_at);
416
+ CREATE INDEX IF NOT EXISTS ${prefix}tasks_lifecycle_status_idx ON ${prefix}tasks(lifecycle_status);
417
+ CREATE INDEX IF NOT EXISTS ${prefix}tasks_stale_after_idx ON ${prefix}tasks(stale_after);
418
+ CREATE INDEX IF NOT EXISTS ${prefix}tasks_last_verified_at_idx ON ${prefix}tasks(last_verified_at);
419
+ `);
420
+ }
354
421
  }
355
422
  ];
356
423
  for (let i = 1; i < MIGRATIONS.length; i++) {
@@ -362,7 +429,29 @@ for (let i = 1; i < MIGRATIONS.length; i++) {
362
429
  }
363
430
  var CURRENT_SCHEMA_VERSION = MIGRATIONS.length > 0 ? MIGRATIONS[MIGRATIONS.length - 1].version : 0;
364
431
 
365
- // src/repositories/EntryRepository.ts
432
+ // src/repositories/rowMappers.ts
433
+ function parseJsonArray(s, fallback) {
434
+ if (Array.isArray(s)) return s;
435
+ if (typeof s === "string" && s.length > 0) {
436
+ try {
437
+ const p = JSON.parse(s);
438
+ if (Array.isArray(p)) return p;
439
+ } catch {
440
+ }
441
+ }
442
+ return fallback;
443
+ }
444
+ function parseJsonObject(s, fallback = null) {
445
+ if (s && typeof s === "object" && !Array.isArray(s)) return s;
446
+ if (typeof s === "string" && s.length > 0) {
447
+ try {
448
+ const p = JSON.parse(s);
449
+ if (p && typeof p === "object" && !Array.isArray(p)) return p;
450
+ } catch {
451
+ }
452
+ }
453
+ return fallback;
454
+ }
366
455
  function mapRowToFact(row) {
367
456
  const tags = (() => {
368
457
  if (Array.isArray(row.tags)) return row.tags;
@@ -373,6 +462,9 @@ function mapRowToFact(row) {
373
462
  }
374
463
  return [];
375
464
  })();
465
+ const okf_verified = parseJsonArray(row.okf_verified, []);
466
+ const now = Date.now();
467
+ const staleAfterRaw = row.stale_after != null ? Number(row.stale_after) : null;
376
468
  return {
377
469
  id: row.id,
378
470
  entity_id: row.entity_id,
@@ -388,7 +480,19 @@ function mapRowToFact(row) {
388
480
  last_accessed_at: row.last_accessed_at === null || row.last_accessed_at === void 0 ? null : Number(row.last_accessed_at),
389
481
  deleted_at: row.deleted_at != null ? Number(row.deleted_at) : null,
390
482
  access_count: Number(row.access_count ?? 0),
391
- okf_type: row.okf_type ?? null
483
+ okf_type: row.okf_type ?? null,
484
+ // OKF v0.2
485
+ lifecycle_status: row.lifecycle_status ?? "stable",
486
+ stale_after: staleAfterRaw,
487
+ generated_by: row.generated_by ?? null,
488
+ okf_sources: parseJsonArray(row.okf_sources, []),
489
+ okf_verified,
490
+ okf_usage_window: parseJsonObject(row.okf_usage_window),
491
+ last_verified_at: row.last_verified_at != null ? Number(row.last_verified_at) : null,
492
+ last_verified_by: row.last_verified_by ?? null,
493
+ // Spec §2.7 + §5.3: hydrate so read() consumers don't re-call the helpers.
494
+ isStale: isStaleAfter(staleAfterRaw, now),
495
+ trustTier: deriveTrustTier(okf_verified)
392
496
  };
393
497
  }
394
498
  function normalizeEmbeddingBlobValue(blob) {
@@ -444,6 +548,21 @@ var EntryRepository = class extends BaseRepository {
444
548
  * Upsert a WikiFact. Nullable fields set to null when fact value is null.
445
549
  * Returns { changes, lastInsertRowId }.
446
550
  * `tx` is REQUIRED to ensure atomic outbox staging.
551
+ *
552
+ * OKF metadata discipline (spec §2.5 + DAO checklist §8): content writes
553
+ * MUST NOT silently overwrite OKF metadata that a caller didn't supply. The
554
+ * previous CASE WHEN IS NULL guards around `lifecycle_status`, etc. were
555
+ * dead because the bind always supplied a non-null value (notably
556
+ * `fact.lifecycle_status ?? 'stable'`), so excluded.X was never NULL and
557
+ * an UPDATE rewrote an existing `deprecated` row to `stable`. We now omit
558
+ * these columns from the SET clause entirely — SQLite's ON CONFLICT UPDATE
559
+ * leaves untouched columns at their existing row value, which is exactly
560
+ * the preservation semantics we want for content writes. Callers that
561
+ * intentionally want to overwrite OKF metadata should use
562
+ * {@link upsertForImport} (always-replace semantics for that path).
563
+ * New-row INSERTs still bind a value so the column is populated; if the
564
+ * caller supplied nothing, the schema DEFAULT 'stable' applies when the
565
+ * bind is bound as 'stable' explicitly here.
447
566
  */
448
567
  async upsert(fact, tx) {
449
568
  const executor = this.getExecutor(tx);
@@ -451,7 +570,10 @@ var EntryRepository = class extends BaseRepository {
451
570
  const tagsJson = JSON.stringify(fact.tags);
452
571
  const embeddingBlob = this.normalizeEmbeddingBlob(fact.embedding_blob);
453
572
  const existingRow = await executor.getFirstAsync(
454
- `SELECT id FROM ${this.prefix}entries WHERE id = ?`,
573
+ `SELECT id, lifecycle_status, stale_after, generated_by,
574
+ okf_sources, okf_verified, okf_usage_window,
575
+ last_verified_at, last_verified_by
576
+ FROM ${this.prefix}entries WHERE id = ?`,
455
577
  [fact.id]
456
578
  );
457
579
  const operation = fact.deleted_at ? "DELETE" : existingRow ? "UPDATE" : "INSERT";
@@ -459,8 +581,10 @@ var EntryRepository = class extends BaseRepository {
459
581
  `INSERT INTO ${this.prefix}entries (
460
582
  id, entity_id, title, body, tags, confidence, source_type,
461
583
  source_hash, source_ref, created_at, updated_at, last_accessed_at, access_count,
462
- deleted_at, embedding_blob, embedding, okf_type
463
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
584
+ deleted_at, embedding_blob, embedding, okf_type,
585
+ lifecycle_status, stale_after, generated_by, last_verified_at, last_verified_by,
586
+ okf_sources, okf_verified, okf_usage_window
587
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
464
588
  ON CONFLICT(id) DO UPDATE SET
465
589
  entity_id = excluded.entity_id,
466
590
  title = excluded.title,
@@ -494,15 +618,36 @@ var EntryRepository = class extends BaseRepository {
494
618
  fact.deleted_at ?? null,
495
619
  embeddingBlob ?? null,
496
620
  null,
497
- fact.okf_type ?? null
621
+ fact.okf_type ?? null,
622
+ // OKF v0.2 metadata — bound for INSERT only; ON CONFLICT UPDATE
623
+ // preserves the existing row values (these columns are intentionally
624
+ // absent from the SET clause above).
625
+ fact.lifecycle_status ?? "stable",
626
+ fact.stale_after ?? null,
627
+ fact.generated_by ?? null,
628
+ fact.last_verified_at ?? null,
629
+ fact.last_verified_by ?? null,
630
+ fact.okf_sources ? JSON.stringify(fact.okf_sources) : null,
631
+ fact.okf_verified ? JSON.stringify(fact.okf_verified) : null,
632
+ fact.okf_usage_window ? JSON.stringify(fact.okf_usage_window) : null
498
633
  ]
499
634
  );
635
+ const persistedOkf = existingRow ? {
636
+ lifecycle_status: existingRow.lifecycle_status ?? "stable",
637
+ stale_after: existingRow.stale_after != null ? Number(existingRow.stale_after) : null,
638
+ generated_by: existingRow.generated_by ?? null,
639
+ okf_sources: parseJsonArray(existingRow.okf_sources, []),
640
+ okf_verified: parseJsonArray(existingRow.okf_verified, []),
641
+ okf_usage_window: parseJsonObject(existingRow.okf_usage_window),
642
+ last_verified_at: existingRow.last_verified_at != null ? Number(existingRow.last_verified_at) : null,
643
+ last_verified_by: existingRow.last_verified_by ?? null
644
+ } : null;
500
645
  await this.outbox.push({
501
646
  entityId: fact.entity_id,
502
647
  tableName: "entries",
503
648
  recordId: fact.id,
504
649
  operation,
505
- payload: fact
650
+ payload: persistedOkf ? { ...fact, ...persistedOkf } : fact
506
651
  }, tx);
507
652
  return result;
508
653
  }
@@ -563,8 +708,10 @@ var EntryRepository = class extends BaseRepository {
563
708
  `INSERT INTO ${this.prefix}entries (
564
709
  id, entity_id, title, body, tags, confidence, source_type,
565
710
  source_hash, source_ref, created_at, updated_at, last_accessed_at, access_count,
566
- deleted_at, embedding_blob, embedding, okf_type
567
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
711
+ deleted_at, embedding_blob, embedding, okf_type,
712
+ lifecycle_status, stale_after, generated_by, last_verified_at, last_verified_by,
713
+ okf_sources, okf_verified, okf_usage_window
714
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
568
715
  ON CONFLICT(id) DO UPDATE SET
569
716
  entity_id = excluded.entity_id,
570
717
  title = excluded.title,
@@ -581,7 +728,15 @@ var EntryRepository = class extends BaseRepository {
581
728
  deleted_at = excluded.deleted_at,
582
729
  embedding_blob = excluded.embedding_blob,
583
730
  embedding = NULL,
584
- okf_type = excluded.okf_type`,
731
+ okf_type = excluded.okf_type,
732
+ lifecycle_status = excluded.lifecycle_status,
733
+ stale_after = excluded.stale_after,
734
+ generated_by = excluded.generated_by,
735
+ last_verified_at = excluded.last_verified_at,
736
+ last_verified_by = excluded.last_verified_by,
737
+ okf_sources = excluded.okf_sources,
738
+ okf_verified = excluded.okf_verified,
739
+ okf_usage_window = excluded.okf_usage_window`,
585
740
  [
586
741
  fact.id,
587
742
  fact.entity_id,
@@ -599,7 +754,16 @@ var EntryRepository = class extends BaseRepository {
599
754
  fact.deleted_at ?? null,
600
755
  embeddingBlob ?? null,
601
756
  null,
602
- fact.okf_type ?? null
757
+ fact.okf_type ?? null,
758
+ // Same NOT NULL DEFAULT note as in upsert above.
759
+ fact.lifecycle_status ?? "stable",
760
+ fact.stale_after ?? null,
761
+ fact.generated_by ?? null,
762
+ fact.last_verified_at ?? null,
763
+ fact.last_verified_by ?? null,
764
+ fact.okf_sources ? JSON.stringify(fact.okf_sources) : null,
765
+ fact.okf_verified ? JSON.stringify(fact.okf_verified) : null,
766
+ fact.okf_usage_window ? JSON.stringify(fact.okf_usage_window) : null
603
767
  ]
604
768
  );
605
769
  return result;
@@ -1527,9 +1691,10 @@ var SourceRefIndexRepository = class extends BaseRepository {
1527
1691
  return row?.source_ref ?? null;
1528
1692
  }
1529
1693
  };
1530
-
1531
- // src/repositories/TaskRepository.ts
1532
1694
  function mapRowToTask(row) {
1695
+ const okf_verified = parseJsonArray(row.okf_verified, []);
1696
+ const now = Date.now();
1697
+ const staleAfterRaw = row.stale_after != null ? Number(row.stale_after) : null;
1533
1698
  return {
1534
1699
  id: row.id,
1535
1700
  entity_id: row.entity_id,
@@ -1540,7 +1705,19 @@ function mapRowToTask(row) {
1540
1705
  updated_at: Number(row.updated_at),
1541
1706
  resolved_at: row.resolved_at != null ? Number(row.resolved_at) : null,
1542
1707
  deleted_at: row.deleted_at != null ? Number(row.deleted_at) : null,
1543
- okf_type: row.okf_type ?? null
1708
+ okf_type: row.okf_type ?? null,
1709
+ // OKF v0.2
1710
+ lifecycle_status: row.lifecycle_status ?? "stable",
1711
+ stale_after: staleAfterRaw,
1712
+ generated_by: row.generated_by ?? null,
1713
+ okf_sources: parseJsonArray(row.okf_sources, []),
1714
+ okf_verified,
1715
+ okf_usage_window: parseJsonObject(row.okf_usage_window),
1716
+ last_verified_at: row.last_verified_at != null ? Number(row.last_verified_at) : null,
1717
+ last_verified_by: row.last_verified_by ?? null,
1718
+ // Spec §2.7 + §5.3: hydrate so read() consumers don't re-call the helpers.
1719
+ isStale: isStaleAfter(staleAfterRaw, now),
1720
+ trustTier: deriveTrustTier(okf_verified)
1544
1721
  };
1545
1722
  }
1546
1723
  var TaskRepository = class extends BaseRepository {
@@ -1590,20 +1767,31 @@ var TaskRepository = class extends BaseRepository {
1590
1767
  * Uses ON CONFLICT(id) DO UPDATE (not INSERT OR REPLACE).
1591
1768
  * Stages an outbox entry in the same transaction.
1592
1769
  * `tx` is REQUIRED.
1770
+ *
1771
+ * OKF metadata (lifecycle_status, sources, etc.) is intentionally excluded
1772
+ * from the SET clause so content writes preserve any existing row values.
1773
+ * See {@link EntryRepository.upsert} for the rationale — the same applies
1774
+ * symmetrically to tasks (spec §2.5). Callers that need to overwrite OKF
1775
+ * metadata should use {@link upsertForImport}.
1593
1776
  */
1594
1777
  async upsert(task, tx, updatedAt) {
1595
1778
  const executor = this.getExecutor(tx);
1596
1779
  const now = Number.isFinite(updatedAt) ? updatedAt : Date.now();
1597
1780
  const existingRow = await executor.getFirstAsync(
1598
- `SELECT id FROM ${this.prefix}tasks WHERE id = ?`,
1781
+ `SELECT id, lifecycle_status, stale_after, generated_by,
1782
+ okf_sources, okf_verified, okf_usage_window,
1783
+ last_verified_at, last_verified_by
1784
+ FROM ${this.prefix}tasks WHERE id = ?`,
1599
1785
  [task.id]
1600
1786
  );
1601
1787
  const operation = task.deleted_at != null ? "DELETE" : existingRow ? "UPDATE" : "INSERT";
1602
1788
  await executor.runAsync(
1603
1789
  `INSERT INTO ${this.prefix}tasks (
1604
1790
  id, entity_id, description, status, priority,
1605
- created_at, updated_at, resolved_at, deleted_at
1606
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1791
+ created_at, updated_at, resolved_at, deleted_at,
1792
+ lifecycle_status, stale_after, generated_by, last_verified_at, last_verified_by,
1793
+ okf_sources, okf_verified, okf_usage_window
1794
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1607
1795
  ON CONFLICT(id) DO UPDATE SET
1608
1796
  entity_id = excluded.entity_id,
1609
1797
  description = excluded.description,
@@ -1622,16 +1810,37 @@ var TaskRepository = class extends BaseRepository {
1622
1810
  now,
1623
1811
  // updated_at set by repo or import override
1624
1812
  task.resolved_at ?? null,
1625
- task.deleted_at ?? null
1813
+ task.deleted_at ?? null,
1814
+ // OKF v0.2 metadata — bound for INSERT only; ON CONFLICT UPDATE
1815
+ // preserves the existing row values (these columns are intentionally
1816
+ // absent from the SET clause above).
1817
+ task.lifecycle_status ?? "stable",
1818
+ task.stale_after ?? null,
1819
+ task.generated_by ?? null,
1820
+ task.last_verified_at ?? null,
1821
+ task.last_verified_by ?? null,
1822
+ task.okf_sources ? JSON.stringify(task.okf_sources) : null,
1823
+ task.okf_verified ? JSON.stringify(task.okf_verified) : null,
1824
+ task.okf_usage_window ? JSON.stringify(task.okf_usage_window) : null
1626
1825
  ]
1627
1826
  );
1827
+ const persistedOkf = existingRow ? {
1828
+ lifecycle_status: existingRow.lifecycle_status ?? "stable",
1829
+ stale_after: existingRow.stale_after != null ? Number(existingRow.stale_after) : null,
1830
+ generated_by: existingRow.generated_by ?? null,
1831
+ okf_sources: parseJsonArray(existingRow.okf_sources, []),
1832
+ okf_verified: parseJsonArray(existingRow.okf_verified, []),
1833
+ okf_usage_window: parseJsonObject(existingRow.okf_usage_window),
1834
+ last_verified_at: existingRow.last_verified_at != null ? Number(existingRow.last_verified_at) : null,
1835
+ last_verified_by: existingRow.last_verified_by ?? null
1836
+ } : null;
1628
1837
  await this.outbox.push(
1629
1838
  {
1630
1839
  entityId: task.entity_id,
1631
1840
  tableName: "tasks",
1632
1841
  recordId: task.id,
1633
1842
  operation,
1634
- payload: task
1843
+ payload: persistedOkf ? { ...task, ...persistedOkf } : task
1635
1844
  },
1636
1845
  tx
1637
1846
  );
@@ -1642,8 +1851,10 @@ var TaskRepository = class extends BaseRepository {
1642
1851
  await executor.runAsync(
1643
1852
  `INSERT INTO ${this.prefix}tasks (
1644
1853
  id, entity_id, description, status, priority,
1645
- created_at, updated_at, resolved_at, deleted_at, okf_type
1646
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1854
+ created_at, updated_at, resolved_at, deleted_at, okf_type,
1855
+ lifecycle_status, stale_after, generated_by, last_verified_at, last_verified_by,
1856
+ okf_sources, okf_verified, okf_usage_window
1857
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1647
1858
  ON CONFLICT(id) DO UPDATE SET
1648
1859
  entity_id = excluded.entity_id,
1649
1860
  description = excluded.description,
@@ -1652,7 +1863,15 @@ var TaskRepository = class extends BaseRepository {
1652
1863
  updated_at = excluded.updated_at,
1653
1864
  resolved_at = excluded.resolved_at,
1654
1865
  deleted_at = excluded.deleted_at,
1655
- okf_type = excluded.okf_type`,
1866
+ okf_type = excluded.okf_type,
1867
+ lifecycle_status = excluded.lifecycle_status,
1868
+ stale_after = excluded.stale_after,
1869
+ generated_by = excluded.generated_by,
1870
+ last_verified_at = excluded.last_verified_at,
1871
+ last_verified_by = excluded.last_verified_by,
1872
+ okf_sources = excluded.okf_sources,
1873
+ okf_verified = excluded.okf_verified,
1874
+ okf_usage_window = excluded.okf_usage_window`,
1656
1875
  [
1657
1876
  task.id,
1658
1877
  task.entity_id,
@@ -1663,7 +1882,15 @@ var TaskRepository = class extends BaseRepository {
1663
1882
  now,
1664
1883
  task.resolved_at ?? null,
1665
1884
  task.deleted_at ?? null,
1666
- task.okf_type ?? null
1885
+ task.okf_type ?? null,
1886
+ task.lifecycle_status ?? "stable",
1887
+ task.stale_after ?? null,
1888
+ task.generated_by ?? null,
1889
+ task.last_verified_at ?? null,
1890
+ task.last_verified_by ?? null,
1891
+ task.okf_sources ? JSON.stringify(task.okf_sources) : null,
1892
+ task.okf_verified ? JSON.stringify(task.okf_verified) : null,
1893
+ task.okf_usage_window ? JSON.stringify(task.okf_usage_window) : null
1667
1894
  ]
1668
1895
  );
1669
1896
  }
@@ -2239,6 +2466,164 @@ var GraphTraversalService = class {
2239
2466
  return { nodes, edges: filteredEdges };
2240
2467
  }
2241
2468
  };
2469
+ var OkfTrustWritesRepository = class {
2470
+ constructor(db, prefix) {
2471
+ this.db = db;
2472
+ this.prefix = prefix;
2473
+ }
2474
+ getExecutor(tx) {
2475
+ return tx ?? this.db;
2476
+ }
2477
+ /**
2478
+ * Append one or more verification events to a fact's `okf_verified` JSON
2479
+ * array, atomically, in a single SQL statement.
2480
+ *
2481
+ * The implementation uses json_each over the existing array UNION ALL'd
2482
+ * with json_each over the new events wrapped in json_group_array — a single
2483
+ * UPDATE expression. SQLite serializes this against concurrent writers on
2484
+ * the same row, so two callers appending simultaneously cannot lose each
2485
+ * other's events (the previous implementation serialized only the supplied
2486
+ * array and overwrote, dropping any pre-existing history).
2487
+ *
2488
+ * Sets the `last_verified_*` convenience columns from the latest event in
2489
+ * the union (computed in code; see {@link latestVerified}).
2490
+ */
2491
+ async writeOkfTrust(entryId, entityId, verified, tx) {
2492
+ const executor = this.getExecutor(tx);
2493
+ if (verified.length === 0) return;
2494
+ const latest = latestVerified(verified, Date.now());
2495
+ const newEventsJson = JSON.stringify(verified);
2496
+ await executor.runAsync(
2497
+ `UPDATE ${this.prefix}entries
2498
+ SET okf_verified = COALESCE(
2499
+ (SELECT json_group_array(json_object('by', s.by_val, 'at', s.at_val))
2500
+ FROM (
2501
+ SELECT json_extract(value, '$.by') AS by_val,
2502
+ json_extract(value, '$.at') AS at_val
2503
+ FROM json_each(okf_verified)
2504
+ UNION ALL
2505
+ SELECT json_extract(value, '$.by'),
2506
+ json_extract(value, '$.at')
2507
+ FROM json_each(?)
2508
+ ) AS s
2509
+ ORDER BY s.at_val),
2510
+ '[]'
2511
+ ),
2512
+ last_verified_by = ?,
2513
+ last_verified_at = ?
2514
+ WHERE id = ? AND entity_id = ?`,
2515
+ [newEventsJson, latest?.by ?? null, latest?.at ?? null, entryId, entityId]
2516
+ );
2517
+ }
2518
+ /** Replace the `sources` list (provenance). */
2519
+ async writeOkfSources(entryId, entityId, sources, tx) {
2520
+ const executor = this.getExecutor(tx);
2521
+ const json = formatSourcesJson(sources);
2522
+ await executor.runAsync(
2523
+ `UPDATE ${this.prefix}entries
2524
+ SET okf_sources = ?
2525
+ WHERE id = ? AND entity_id = ?`,
2526
+ [json, entryId, entityId]
2527
+ );
2528
+ }
2529
+ /** Set the OKF v0.2 lifecycle status. */
2530
+ async setLifecycleStatus(entryId, entityId, status, tx) {
2531
+ const executor = this.getExecutor(tx);
2532
+ await executor.runAsync(
2533
+ `UPDATE ${this.prefix}entries
2534
+ SET lifecycle_status = ?
2535
+ WHERE id = ? AND entity_id = ?`,
2536
+ [status, entryId, entityId]
2537
+ );
2538
+ }
2539
+ /** Set the absolute cutoff (epoch ms) for staleness, or NULL to clear. */
2540
+ async setStaleAfter(entryId, entityId, date, tx) {
2541
+ const executor = this.getExecutor(tx);
2542
+ await executor.runAsync(
2543
+ `UPDATE ${this.prefix}entries
2544
+ SET stale_after = ?
2545
+ WHERE id = ? AND entity_id = ?`,
2546
+ [date, entryId, entityId]
2547
+ );
2548
+ }
2549
+ /** Set the producer / verifier actor string. Rare. */
2550
+ async setGeneratedBy(entryId, entityId, actor, tx) {
2551
+ const executor = this.getExecutor(tx);
2552
+ await executor.runAsync(
2553
+ `UPDATE ${this.prefix}entries
2554
+ SET generated_by = ?
2555
+ WHERE id = ? AND entity_id = ?`,
2556
+ [actor, entryId, entityId]
2557
+ );
2558
+ }
2559
+ // The same five methods exist for tasks, with `tasks` instead of `entries`.
2560
+ // Tasks are symmetric per spec §2.5.
2561
+ /** Atomic append, task variant — mirrors {@link writeOkfTrust}. */
2562
+ async writeOkfTrustTask(taskId, entityId, verified, tx) {
2563
+ const executor = this.getExecutor(tx);
2564
+ if (verified.length === 0) return;
2565
+ const latest = latestVerified(verified, Date.now());
2566
+ const newEventsJson = JSON.stringify(verified);
2567
+ await executor.runAsync(
2568
+ `UPDATE ${this.prefix}tasks
2569
+ SET okf_verified = COALESCE(
2570
+ (SELECT json_group_array(json_object('by', s.by_val, 'at', s.at_val))
2571
+ FROM (
2572
+ SELECT json_extract(value, '$.by') AS by_val,
2573
+ json_extract(value, '$.at') AS at_val
2574
+ FROM json_each(okf_verified)
2575
+ UNION ALL
2576
+ SELECT json_extract(value, '$.by'),
2577
+ json_extract(value, '$.at')
2578
+ FROM json_each(?)
2579
+ ) AS s
2580
+ ORDER BY s.at_val),
2581
+ '[]'
2582
+ ),
2583
+ last_verified_by = ?,
2584
+ last_verified_at = ?
2585
+ WHERE id = ? AND entity_id = ?`,
2586
+ [newEventsJson, latest?.by ?? null, latest?.at ?? null, taskId, entityId]
2587
+ );
2588
+ }
2589
+ async writeOkfSourcesTask(taskId, entityId, sources, tx) {
2590
+ const executor = this.getExecutor(tx);
2591
+ const json = formatSourcesJson(sources);
2592
+ await executor.runAsync(
2593
+ `UPDATE ${this.prefix}tasks
2594
+ SET okf_sources = ?
2595
+ WHERE id = ? AND entity_id = ?`,
2596
+ [json, taskId, entityId]
2597
+ );
2598
+ }
2599
+ async setLifecycleStatusTask(taskId, entityId, status, tx) {
2600
+ const executor = this.getExecutor(tx);
2601
+ await executor.runAsync(
2602
+ `UPDATE ${this.prefix}tasks
2603
+ SET lifecycle_status = ?
2604
+ WHERE id = ? AND entity_id = ?`,
2605
+ [status, taskId, entityId]
2606
+ );
2607
+ }
2608
+ async setStaleAfterTask(taskId, entityId, date, tx) {
2609
+ const executor = this.getExecutor(tx);
2610
+ await executor.runAsync(
2611
+ `UPDATE ${this.prefix}tasks
2612
+ SET stale_after = ?
2613
+ WHERE id = ? AND entity_id = ?`,
2614
+ [date, taskId, entityId]
2615
+ );
2616
+ }
2617
+ async setGeneratedByTask(taskId, entityId, actor, tx) {
2618
+ const executor = this.getExecutor(tx);
2619
+ await executor.runAsync(
2620
+ `UPDATE ${this.prefix}tasks
2621
+ SET generated_by = ?
2622
+ WHERE id = ? AND entity_id = ?`,
2623
+ [actor, taskId, entityId]
2624
+ );
2625
+ }
2626
+ };
2242
2627
 
2243
2628
  // src/WikiMemory.ts
2244
2629
  var TABLE_PREFIX_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,30}_$/;
@@ -2333,6 +2718,7 @@ var WikiMemory = class {
2333
2718
  this.entryRepo,
2334
2719
  this.options.config ?? {}
2335
2720
  );
2721
+ this.okfTrustWrites = new OkfTrustWritesRepository(this.db, this.prefix);
2336
2722
  }
2337
2723
  /**
2338
2724
  * Explicit escape hatch for test suites: typed access to composed services for mocks/spies.
@@ -2700,6 +3086,42 @@ var WikiMemory = class {
2700
3086
  );
2701
3087
  this.ontologyService.invalidateCache(entityId);
2702
3088
  }
3089
+ /** Append a verification event to a fact. Does NOT touch `updated_at`. */
3090
+ async writeOkfTrust(entryId, entityId, verified) {
3091
+ return this.okfTrustWrites.writeOkfTrust(entryId, entityId, verified);
3092
+ }
3093
+ /** Replace a fact's source list. Does NOT touch `updated_at`. */
3094
+ async writeOkfSources(entryId, entityId, sources) {
3095
+ return this.okfTrustWrites.writeOkfSources(entryId, entityId, sources);
3096
+ }
3097
+ /** Set a fact's OKF v0.2 lifecycle status. Does NOT touch `updated_at`. */
3098
+ async setLifecycleStatus(entryId, entityId, status) {
3099
+ return this.okfTrustWrites.setLifecycleStatus(entryId, entityId, status);
3100
+ }
3101
+ /** Set a fact's stale_after (epoch ms) or clear it. Does NOT touch `updated_at`. */
3102
+ async setStaleAfter(entryId, entityId, date) {
3103
+ return this.okfTrustWrites.setStaleAfter(entryId, entityId, date);
3104
+ }
3105
+ /** Set a fact's generated_by actor string. Does NOT touch `updated_at`. */
3106
+ async setGeneratedBy(entryId, entityId, actor) {
3107
+ return this.okfTrustWrites.setGeneratedBy(entryId, entityId, actor);
3108
+ }
3109
+ /** Task variants of the same five DAO methods. Symmetric per spec §2.5. */
3110
+ async writeOkfTrustTask(taskId, entityId, verified) {
3111
+ return this.okfTrustWrites.writeOkfTrustTask(taskId, entityId, verified);
3112
+ }
3113
+ async writeOkfSourcesTask(taskId, entityId, sources) {
3114
+ return this.okfTrustWrites.writeOkfSourcesTask(taskId, entityId, sources);
3115
+ }
3116
+ async setLifecycleStatusTask(taskId, entityId, status) {
3117
+ return this.okfTrustWrites.setLifecycleStatusTask(taskId, entityId, status);
3118
+ }
3119
+ async setStaleAfterTask(taskId, entityId, date) {
3120
+ return this.okfTrustWrites.setStaleAfterTask(taskId, entityId, date);
3121
+ }
3122
+ async setGeneratedByTask(taskId, entityId, actor) {
3123
+ return this.okfTrustWrites.setGeneratedByTask(taskId, entityId, actor);
3124
+ }
2703
3125
  };
2704
3126
  _testAccessNonTestEnvWarned = new WeakMap();
2705
3127
 
@@ -3003,13 +3425,87 @@ function formatMemoryDump(dump) {
3003
3425
  files
3004
3426
  };
3005
3427
  }
3006
- var LLM_WIKI_PROFILE = "llm-wiki/1";
3007
- function factFrontmatter(f) {
3428
+ var DEFAULT_PROFILE = "llm-wiki/2";
3429
+ function isoOrFallback(updatedAt) {
3430
+ if (!Number.isFinite(updatedAt)) return (/* @__PURE__ */ new Date(0)).toISOString();
3431
+ const d = new Date(updatedAt);
3432
+ if (!Number.isFinite(d.getTime())) return (/* @__PURE__ */ new Date(0)).toISOString();
3433
+ return d.toISOString();
3434
+ }
3435
+ function isoDateOrOmit(ms) {
3436
+ if (ms == null || !Number.isFinite(ms)) return void 0;
3437
+ const d = new Date(ms);
3438
+ if (!Number.isFinite(d.getTime())) return void 0;
3439
+ return d.toISOString().slice(0, 10);
3440
+ }
3441
+ function factFrontmatterV2(f) {
3008
3442
  return {
3009
3443
  type: f.okf_type ?? "fact",
3010
3444
  title: f.title,
3011
3445
  tags: f.tags,
3012
- timestamp: new Date(f.updated_at).toISOString(),
3446
+ // `timestamp` rides along deliberately even on the v0.2 path — a naive
3447
+ // consumer that only understands the v0.1 key still gets a readable date
3448
+ // without needing flow-mapping support. `generated.at` is the canonical
3449
+ // v0.2 field and always wins on import when both are present (spec §2.4).
3450
+ timestamp: isoOrFallback(f.updated_at),
3451
+ resource: f.source_ref ?? void 0,
3452
+ id: f.id,
3453
+ entity_id: f.entity_id,
3454
+ confidence: f.confidence,
3455
+ source_type: f.source_type,
3456
+ source_hash: f.source_hash,
3457
+ created_at: f.created_at,
3458
+ access_count: f.access_count,
3459
+ last_accessed_at: f.last_accessed_at,
3460
+ deleted_at: f.deleted_at,
3461
+ // OKF v0.2
3462
+ status: f.lifecycle_status ?? "stable",
3463
+ ...isoDateOrOmit(f.stale_after) ? { stale_after: isoDateOrOmit(f.stale_after) } : {},
3464
+ // Omit `generated` entirely when there's no actor on record — never
3465
+ // fabricate a `generated_by` (mirrors the import side's rule in §4.8:
3466
+ // it never invents one either, so export must not manufacture one that
3467
+ // would then round-trip back in as if it had always been asserted).
3468
+ ...f.generated_by ? { generated: { by: f.generated_by, at: isoOrFallback(f.updated_at) } } : {},
3469
+ // Omit `verified`/`sources` when empty — an emitted `[]` is not the same
3470
+ // shape as an absent key (spec §5.3 derives `unverified` from "no
3471
+ // `verified` key", not from "empty `verified` list").
3472
+ ...f.okf_verified && f.okf_verified.length > 0 ? { verified: f.okf_verified } : {},
3473
+ ...f.okf_sources && f.okf_sources.length > 0 ? { sources: f.okf_sources } : {},
3474
+ ...f.okf_usage_window ? { usage_window: f.okf_usage_window } : {}
3475
+ };
3476
+ }
3477
+ function taskFrontmatterV2(t) {
3478
+ return {
3479
+ type: t.okf_type ?? "task",
3480
+ title: t.description,
3481
+ timestamp: isoOrFallback(t.updated_at),
3482
+ // see factFrontmatterV2 comment — deliberate back-compat duplication
3483
+ id: t.id,
3484
+ entity_id: t.entity_id,
3485
+ status: t.lifecycle_status ?? "stable",
3486
+ // OKF v0.2 lifecycle
3487
+ execution_status: t.status,
3488
+ // execution state under renamed key
3489
+ priority: t.priority,
3490
+ created_at: t.created_at,
3491
+ resolved_at: t.resolved_at,
3492
+ deleted_at: t.deleted_at,
3493
+ // OKF v0.2 — symmetric with facts (spec §2.5): stale_after applies to
3494
+ // tasks too (a hallucinated task needs a freshness signal as much as a
3495
+ // hallucinated fact does).
3496
+ ...isoDateOrOmit(t.stale_after) ? { stale_after: isoDateOrOmit(t.stale_after) } : {},
3497
+ ...t.generated_by ? { generated: { by: t.generated_by, at: isoOrFallback(t.updated_at) } } : {},
3498
+ ...t.okf_verified && t.okf_verified.length > 0 ? { verified: t.okf_verified } : {},
3499
+ ...t.okf_sources && t.okf_sources.length > 0 ? { sources: t.okf_sources } : {},
3500
+ ...t.okf_usage_window ? { usage_window: t.okf_usage_window } : {}
3501
+ };
3502
+ }
3503
+ function factFrontmatterV1(f) {
3504
+ return {
3505
+ type: f.okf_type ?? "fact",
3506
+ title: f.title,
3507
+ tags: f.tags,
3508
+ timestamp: isoOrFallback(f.updated_at),
3013
3509
  resource: f.source_ref ?? void 0,
3014
3510
  id: f.id,
3015
3511
  entity_id: f.entity_id,
@@ -3022,11 +3518,11 @@ function factFrontmatter(f) {
3022
3518
  deleted_at: f.deleted_at
3023
3519
  };
3024
3520
  }
3025
- function taskFrontmatter(t) {
3521
+ function taskFrontmatterV1(t) {
3026
3522
  return {
3027
3523
  type: t.okf_type ?? "task",
3028
3524
  title: t.description,
3029
- timestamp: new Date(t.updated_at).toISOString(),
3525
+ timestamp: isoOrFallback(t.updated_at),
3030
3526
  id: t.id,
3031
3527
  entity_id: t.entity_id,
3032
3528
  status: t.status,
@@ -3056,7 +3552,11 @@ function buildEventLogEntries(events, factIdToFilename) {
3056
3552
  return { date: formatLogDate(e.created_at), text: appendEventIdComment(text, e.id) };
3057
3553
  });
3058
3554
  }
3059
- function formatOkfBundle(dump) {
3555
+ function formatOkfBundle(dump, options) {
3556
+ const profile = options?.profile ?? DEFAULT_PROFILE;
3557
+ const okfVersion = profile === "llm-wiki/2" ? "0.2" : "0.1";
3558
+ const factFm = profile === "llm-wiki/2" ? factFrontmatterV2 : factFrontmatterV1;
3559
+ const taskFm = profile === "llm-wiki/2" ? taskFrontmatterV2 : taskFrontmatterV1;
3060
3560
  const files = [];
3061
3561
  const rootEntries = [];
3062
3562
  for (const [entityId, bundle] of Object.entries(dump.entities)) {
@@ -3093,7 +3593,7 @@ function formatOkfBundle(dump) {
3093
3593
  const factBody = appendRelatedSection(f.body, relatedLinksFor2(factPath, f.id));
3094
3594
  files.push({
3095
3595
  path: factPath,
3096
- content: buildConceptDocument(factFrontmatter(f), factBody)
3596
+ content: buildConceptDocument(factFm(f), factBody)
3097
3597
  });
3098
3598
  }
3099
3599
  const taskEntries = bundle.tasks.map((t) => ({
@@ -3105,7 +3605,7 @@ function formatOkfBundle(dump) {
3105
3605
  const taskBody = appendRelatedSection("", relatedLinksFor2(taskPath, t.id));
3106
3606
  files.push({
3107
3607
  path: taskPath,
3108
- content: buildConceptDocument(taskFrontmatter(t), taskBody)
3608
+ content: buildConceptDocument(taskFm(t), taskBody)
3109
3609
  });
3110
3610
  }
3111
3611
  files.push({
@@ -3128,9 +3628,9 @@ function formatOkfBundle(dump) {
3128
3628
  files.push({
3129
3629
  path: "index.md",
3130
3630
  content: buildRootIndexMd(
3131
- "0.1",
3631
+ okfVersion,
3132
3632
  rootEntries.length > 0 ? [{ heading: "Entities", entries: rootEntries }] : [],
3133
- { profile: LLM_WIKI_PROFILE }
3633
+ { profile }
3134
3634
  )
3135
3635
  });
3136
3636
  return { files };
@@ -3220,6 +3720,21 @@ function parseFrontmatterTimestamp(value, fallback) {
3220
3720
  }
3221
3721
  return fallback;
3222
3722
  }
3723
+ function parseStaleAfterRaw(raw) {
3724
+ if (typeof raw !== "string") return null;
3725
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
3726
+ if (!m) return null;
3727
+ const year = Number(m[1]);
3728
+ const month = Number(m[2]);
3729
+ const day = Number(m[3]);
3730
+ const ms = Date.UTC(year, month - 1, day);
3731
+ if (!Number.isFinite(ms)) return null;
3732
+ const d = new Date(ms);
3733
+ if (d.getUTCFullYear() !== year || d.getUTCMonth() + 1 !== month || d.getUTCDate() !== day) {
3734
+ return null;
3735
+ }
3736
+ return ms;
3737
+ }
3223
3738
  function unescapeLogSummary(summary) {
3224
3739
  return summary.replace(/\\\]/g, "]").replace(/\\\[/g, "[").replace(/\\\\/g, "\\");
3225
3740
  }
@@ -3237,9 +3752,29 @@ function parseLogEntryText(text) {
3237
3752
  function frontmatterToFact(entityId, id, frontmatter, body, now) {
3238
3753
  const created_at = parseFrontmatterTimestamp(frontmatter.created_at, now);
3239
3754
  const updated_at = parseFrontmatterTimestamp(
3240
- frontmatter.timestamp,
3241
- parseFrontmatterTimestamp(frontmatter.updated_at, now)
3755
+ frontmatter.generated?.at,
3756
+ parseFrontmatterTimestamp(
3757
+ frontmatter.timestamp,
3758
+ parseFrontmatterTimestamp(frontmatter.updated_at, now)
3759
+ )
3242
3760
  );
3761
+ const verified = parseVerifiedFlexible(frontmatter.verified);
3762
+ const rawSources = frontmatter.sources;
3763
+ const hasSourcesKey = rawSources !== void 0;
3764
+ const sources = Array.isArray(rawSources) ? rawSources : [];
3765
+ const usageWindow = frontmatter.usage_window && typeof frontmatter.usage_window === "object" ? frontmatter.usage_window : null;
3766
+ const lastV = latestVerified(verified, now);
3767
+ const lifecycleRaw = frontmatter.status;
3768
+ const lifecycle_status = lifecycleRaw === "draft" || lifecycleRaw === "stable" || lifecycleRaw === "deprecated" ? lifecycleRaw : "stable";
3769
+ const staleAfterRaw = frontmatter.stale_after;
3770
+ const stale_after = parseStaleAfterRaw(staleAfterRaw);
3771
+ const generated_by = frontmatter.generated?.by ?? null;
3772
+ if (!hasSourcesKey) {
3773
+ const urls = parseCitationsList(body);
3774
+ for (const url of urls) {
3775
+ sources.push({ resource: url });
3776
+ }
3777
+ }
3243
3778
  return {
3244
3779
  id,
3245
3780
  entity_id: entityId,
@@ -3255,26 +3790,62 @@ function frontmatterToFact(entityId, id, frontmatter, body, now) {
3255
3790
  last_accessed_at: frontmatter.last_accessed_at != null ? parseFrontmatterTimestamp(frontmatter.last_accessed_at, now) : null,
3256
3791
  access_count: typeof frontmatter.access_count === "number" ? frontmatter.access_count : 0,
3257
3792
  deleted_at: frontmatter.deleted_at != null ? parseFrontmatterTimestamp(frontmatter.deleted_at, 0) : null,
3258
- okf_type: frontmatter.type
3793
+ okf_type: frontmatter.type,
3794
+ // OKF v0.2
3795
+ lifecycle_status,
3796
+ stale_after,
3797
+ generated_by,
3798
+ okf_sources: sources,
3799
+ okf_verified: verified,
3800
+ okf_usage_window: usageWindow,
3801
+ last_verified_at: lastV?.at ?? null,
3802
+ last_verified_by: lastV?.by ?? null
3259
3803
  };
3260
3804
  }
3261
- function frontmatterToTask(entityId, id, frontmatter, now) {
3805
+ function frontmatterToTask(entityId, id, frontmatter, now, usesV1StatusRule) {
3262
3806
  const created_at = parseFrontmatterTimestamp(frontmatter.created_at, now);
3263
3807
  const updated_at = parseFrontmatterTimestamp(
3264
- frontmatter.timestamp,
3265
- parseFrontmatterTimestamp(frontmatter.updated_at, now)
3808
+ frontmatter.generated?.at,
3809
+ parseFrontmatterTimestamp(
3810
+ frontmatter.timestamp,
3811
+ parseFrontmatterTimestamp(frontmatter.updated_at, now)
3812
+ )
3266
3813
  );
3814
+ const verified = parseVerifiedFlexible(frontmatter.verified);
3815
+ const sources = Array.isArray(frontmatter.sources) ? frontmatter.sources : [];
3816
+ const usageWindow = frontmatter.usage_window && typeof frontmatter.usage_window === "object" ? frontmatter.usage_window : null;
3817
+ const lastV = latestVerified(verified, now);
3818
+ const lifecycleRaw = frontmatter.status;
3819
+ const lifecycle_status = lifecycleRaw === "draft" || lifecycleRaw === "stable" || lifecycleRaw === "deprecated" ? lifecycleRaw : "stable";
3820
+ const staleAfterRaw = frontmatter.stale_after;
3821
+ const stale_after = parseStaleAfterRaw(staleAfterRaw);
3822
+ const executionRaw = frontmatter.execution_status;
3823
+ let executionState;
3824
+ if (usesV1StatusRule) {
3825
+ executionState = TASK_STATUSES.has(String(frontmatter.status)) ? frontmatter.status : "pending";
3826
+ } else {
3827
+ executionState = TASK_STATUSES.has(String(executionRaw)) ? executionRaw : TASK_STATUSES.has(String(frontmatter.status)) ? frontmatter.status : "pending";
3828
+ }
3267
3829
  return {
3268
3830
  id,
3269
3831
  entity_id: entityId,
3270
3832
  description: typeof frontmatter.title === "string" ? frontmatter.title : "",
3271
- status: TASK_STATUSES.has(String(frontmatter.status)) ? frontmatter.status : "pending",
3833
+ status: executionState,
3272
3834
  priority: typeof frontmatter.priority === "number" ? frontmatter.priority : 0,
3273
3835
  created_at,
3274
3836
  updated_at,
3275
3837
  resolved_at: frontmatter.resolved_at != null ? parseFrontmatterTimestamp(frontmatter.resolved_at, now) : null,
3276
3838
  deleted_at: frontmatter.deleted_at != null ? parseFrontmatterTimestamp(frontmatter.deleted_at, 0) : null,
3277
- okf_type: frontmatter.type
3839
+ okf_type: frontmatter.type,
3840
+ // OKF v0.2
3841
+ lifecycle_status,
3842
+ stale_after,
3843
+ generated_by: frontmatter.generated?.by ?? null,
3844
+ okf_sources: sources,
3845
+ okf_verified: verified,
3846
+ okf_usage_window: usageWindow,
3847
+ last_verified_at: lastV?.at ?? null,
3848
+ last_verified_by: lastV?.by ?? null
3278
3849
  };
3279
3850
  }
3280
3851
  function findLogMdPath(files) {
@@ -3321,10 +3892,17 @@ function parseOkfBundle(entityId, files, options) {
3321
3892
  );
3322
3893
  const rootIndex = allowedFiles.find((f) => f.path === "index.md");
3323
3894
  const profileMeta = rootIndex ? parseRootIndexMd(rootIndex.content) : {};
3324
- const isProfile1 = profileMeta.profile === "llm-wiki/1";
3895
+ const okfVersion = profileMeta.okf_version;
3896
+ const profile = profileMeta.profile;
3897
+ const isProfile1 = profile === "llm-wiki/1";
3898
+ const isProfile2 = profile === "llm-wiki/2";
3899
+ const isLegacyV1 = profile === void 0 && okfVersion === "0.1";
3900
+ const isVersionOnlyV2 = profile === void 0 && okfVersion === "0.2";
3901
+ const isUnknownProfileV2 = profile !== void 0 && profile !== "llm-wiki/1" && profile !== "llm-wiki/2" && okfVersion === "0.2";
3902
+ const summaryEligible = isProfile1 || isProfile2 || isLegacyV1 || isVersionOnlyV2 || isUnknownProfileV2;
3325
3903
  let entitySummary;
3326
3904
  const entityIndex = allowedFiles.find((f) => f.path === `${entityPrefix}index.md`);
3327
- if (isProfile1 && entityIndex) {
3905
+ if (summaryEligible && entityIndex) {
3328
3906
  const summary = parseEntityIndexMd(entityIndex.content).summary;
3329
3907
  entitySummary = summary || void 0;
3330
3908
  }
@@ -3353,11 +3931,12 @@ function parseOkfBundle(entityId, files, options) {
3353
3931
  if (route === "ignore") continue;
3354
3932
  const resolvedId = typeof frontmatter.id === "string" && frontmatter.id ? frontmatter.id : basenameMd(file.path);
3355
3933
  const { body: storedBody, relatedLinks } = splitRelatedSection(body);
3356
- const edgeLinks = isProfile1 ? relatedLinks : [...relatedLinks, ...extractMarkdownLinks(storedBody)];
3934
+ const edgeLinks = isProfile1 || isLegacyV1 ? relatedLinks : [...relatedLinks, ...extractMarkdownLinks(storedBody)];
3935
+ const usesV1StatusRule = isProfile1 || isLegacyV1 || profile === void 0 && okfVersion === void 0;
3357
3936
  if (route === "fact") {
3358
3937
  facts.push(frontmatterToFact(entityId, resolvedId, frontmatter, storedBody, now));
3359
3938
  } else {
3360
- tasks.push(frontmatterToTask(entityId, resolvedId, frontmatter, now));
3939
+ tasks.push(frontmatterToTask(entityId, resolvedId, frontmatter, now, usesV1StatusRule));
3361
3940
  }
3362
3941
  const seenEdges = /* @__PURE__ */ new Set();
3363
3942
  extractEdgesFromLinks(