@tenphi/akno-core 0.13.1 → 0.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/bench/language.d.ts +16 -8
  2. package/dist/bench/language.d.ts.map +1 -1
  3. package/dist/context.d.ts +3 -0
  4. package/dist/context.d.ts.map +1 -1
  5. package/dist/context.js.map +1 -1
  6. package/dist/index/indexer.d.ts +10 -0
  7. package/dist/index/indexer.d.ts.map +1 -1
  8. package/dist/index/indexer.js +21 -1
  9. package/dist/index/indexer.js.map +1 -1
  10. package/dist/maintenance/brain-migration.d.ts +4 -1
  11. package/dist/maintenance/brain-migration.d.ts.map +1 -1
  12. package/dist/maintenance/brain-migration.js +29 -6
  13. package/dist/maintenance/brain-migration.js.map +1 -1
  14. package/dist/maintenance/dream.d.ts +2 -0
  15. package/dist/maintenance/dream.d.ts.map +1 -1
  16. package/dist/maintenance/dream.js +270 -64
  17. package/dist/maintenance/dream.js.map +1 -1
  18. package/dist/maintenance/observation-scope.d.ts +74 -0
  19. package/dist/maintenance/observation-scope.d.ts.map +1 -0
  20. package/dist/maintenance/observation-scope.js +259 -0
  21. package/dist/maintenance/observation-scope.js.map +1 -0
  22. package/dist/maintenance/observe.d.ts +2 -0
  23. package/dist/maintenance/observe.d.ts.map +1 -1
  24. package/dist/maintenance/observe.js +28 -24
  25. package/dist/maintenance/observe.js.map +1 -1
  26. package/dist/maintenance/plans.d.ts +20 -1
  27. package/dist/maintenance/plans.d.ts.map +1 -1
  28. package/dist/maintenance/plans.js +251 -35
  29. package/dist/maintenance/plans.js.map +1 -1
  30. package/dist/observations/marker.d.ts +2 -0
  31. package/dist/observations/marker.d.ts.map +1 -1
  32. package/dist/observations/marker.js +16 -4
  33. package/dist/observations/marker.js.map +1 -1
  34. package/dist/observations/projection.d.ts +1 -1
  35. package/dist/observations/projection.d.ts.map +1 -1
  36. package/dist/observations/projection.js +14 -5
  37. package/dist/observations/projection.js.map +1 -1
  38. package/dist/open.d.ts.map +1 -1
  39. package/dist/open.js +5 -0
  40. package/dist/open.js.map +1 -1
  41. package/dist/ops/folder.d.ts.map +1 -1
  42. package/dist/ops/folder.js +3 -0
  43. package/dist/ops/folder.js.map +1 -1
  44. package/dist/ops/forget.d.ts.map +1 -1
  45. package/dist/ops/forget.js +9 -0
  46. package/dist/ops/forget.js.map +1 -1
  47. package/dist/ops/graph.js +5 -1
  48. package/dist/ops/graph.js.map +1 -1
  49. package/dist/ops/move.d.ts.map +1 -1
  50. package/dist/ops/move.js +15 -7
  51. package/dist/ops/move.js.map +1 -1
  52. package/dist/ops/remember.d.ts.map +1 -1
  53. package/dist/ops/remember.js +34 -8
  54. package/dist/ops/remember.js.map +1 -1
  55. package/dist/ops/retain.d.ts.map +1 -1
  56. package/dist/ops/retain.js +119 -2
  57. package/dist/ops/retain.js.map +1 -1
  58. package/dist/ops/write.d.ts.map +1 -1
  59. package/dist/ops/write.js +17 -4
  60. package/dist/ops/write.js.map +1 -1
  61. package/dist/store/db.d.ts.map +1 -1
  62. package/dist/store/db.js +18 -1
  63. package/dist/store/db.js.map +1 -1
  64. package/dist/store/migrations.d.ts +4 -2
  65. package/dist/store/migrations.d.ts.map +1 -1
  66. package/dist/store/migrations.js +41 -2
  67. package/dist/store/migrations.js.map +1 -1
  68. package/dist/write/journal.d.ts +2 -0
  69. package/dist/write/journal.d.ts.map +1 -1
  70. package/dist/write/journal.js +11 -0
  71. package/dist/write/journal.js.map +1 -1
  72. package/dist/write/mutation-receipts.d.ts +32 -0
  73. package/dist/write/mutation-receipts.d.ts.map +1 -0
  74. package/dist/write/mutation-receipts.js +331 -0
  75. package/dist/write/mutation-receipts.js.map +1 -0
  76. package/dist/write/retain.d.ts +6 -2
  77. package/dist/write/retain.d.ts.map +1 -1
  78. package/dist/write/retain.js +86 -13
  79. package/dist/write/retain.js.map +1 -1
  80. package/package.json +2 -2
@@ -7,8 +7,9 @@ import { proseQualifications } from "../kb/prose.js";
7
7
  import { rebuildEvidenceGraph } from "../index/graph.js";
8
8
  import { mergeTopLevelStringArray, serializeYamlString, serializeYamlStringArray, } from "../kb/frontmatter.js";
9
9
  import { runObserveMission } from "./observe.js";
10
+ import { assessObservationScope, } from "./observation-scope.js";
10
11
  import { planBrokenLinks } from "./link-repairs.js";
11
- import { claimKey, findCrossPageConflicts, ineligibleConflictClaims, verifyConflicts, } from "./conflicts.js";
12
+ import { claimKey, conflictClaimIneligibility, findCrossPageConflicts, ineligibleConflictClaims, verifyConflicts, } from "./conflicts.js";
12
13
  import { planContradictions } from "./contradictions.js";
13
14
  import { housekeeping } from "./housekeeping.js";
14
15
  import { ModelClient } from "../models/client.js";
@@ -560,20 +561,28 @@ async function runPhase(ctx, phase, options, report, applied, budget, deferAutom
560
561
  case 'curate': {
561
562
  const policyMatrix = !options.dryRun;
562
563
  const policies = curationPolicies(ctx, options);
563
- const managedItemResult = policies.managed_item === 'off'
564
+ const allowedKinds = new Set(['hygiene', 'synthesis', 'split', 'extract', 'merge'].filter((kind) => policies[kind] !== 'off'));
565
+ // Resolve persisted work before any candidate planner can call a model. In particular,
566
+ // managed-item routing is model-backed; running it first generated usable drafts and then
567
+ // discarded them whenever an older plan won queue selection.
568
+ const mode = policyMatrix ? highestPolicyMode(Object.values(policies)) : null;
569
+ let plan = mode ? findActiveMaintenancePlan(ctx, mode, 'curate') : null;
570
+ if (plan && policyMatrix && !planMatchesPolicies(plan, policies))
571
+ plan = null;
572
+ const managedItemResult = plan
564
573
  ? { drafts: [], report: report.managedItems }
565
- : await planManagedItems(ctx, {
566
- conflictClaims: ineligibleConflictClaims(report.conflicts),
567
- });
574
+ : policies.managed_item === 'off'
575
+ ? { drafts: [], report: report.managedItems }
576
+ : await planManagedItems(ctx, {
577
+ conflictClaims: ineligibleConflictClaims(report.conflicts),
578
+ });
568
579
  report.managedItems = managedItemResult.report;
569
580
  if (managedItemResult.report.outcomes.held > 0) {
570
581
  report.warnings.push(`${managedItemResult.report.outcomes.held} managed-item finding${managedItemResult.report.outcomes.held === 1 ? '' : 's'} require${managedItemResult.report.outcomes.held === 1 ? 's' : ''} inspection; surrounding page bytes were not changed`);
571
582
  }
572
- const allowedKinds = new Set(['hygiene', 'synthesis', 'split', 'extract', 'merge'].filter((kind) => policies[kind] !== 'off'));
573
583
  // A configured mode is how a full scheduled run gets plan-backed curation without
574
584
  // turning the scheduler into a curate-only command. An explicit dry run keeps its
575
585
  // existing read-only path; creating a durable plan needs the service's write handle.
576
- const mode = policyMatrix ? highestPolicyMode(Object.values(policies)) : null;
577
586
  if (policyMatrix && !mode)
578
587
  return 'all curation policies are off';
579
588
  if (!ctx.models.derive.available && !mode) {
@@ -583,9 +592,6 @@ async function runPhase(ctx, phase, options, report, applied, budget, deferAutom
583
592
  if (mode) {
584
593
  // Reuse every unfinished plan, not only autonomous ones. Re-running audit or review must
585
594
  // not spend another model call to rediscover a decision already waiting in the queue.
586
- let plan = findActiveMaintenancePlan(ctx, mode, 'curate');
587
- if (plan && policyMatrix && !planMatchesPolicies(plan, policies))
588
- plan = null;
589
595
  if (plan) {
590
596
  report.curated = plan.items.filter(isGeneralCurationItem).map((item) => ({
591
597
  slug: item.subject,
@@ -742,18 +748,25 @@ async function runPhase(ctx, phase, options, report, applied, budget, deferAutom
742
748
  const adoptPolicy = policyMode(effectiveTransformPolicy(ctx.config, 'adopt', options.mode));
743
749
  if (!adoptPolicy)
744
750
  return 'adopt policy is off';
745
- const result = await planOrphanAdoptions(ctx, {
746
- limit: ctx.config.maintenance.adopt.maxPages,
747
- });
748
- report.adopted = result.adopted;
749
- if (options.dryRun)
751
+ if (options.dryRun) {
752
+ const result = await planOrphanAdoptions(ctx, {
753
+ limit: ctx.config.maintenance.adopt.maxPages,
754
+ });
755
+ report.adopted = result.adopted;
750
756
  return null;
757
+ }
751
758
  const mode = adoptPolicy;
752
759
  let plan = findActiveMaintenancePlan(ctx, mode, 'adopt');
753
760
  if (plan && !planMatchesPolicies(plan, { adopt: adoptPolicy }))
754
761
  plan = null;
755
- if (!plan && result.drafts.length > 0) {
756
- plan = createAdoptionPlan(ctx, mode, result.drafts, report.run.snapshot, adoptPolicy);
762
+ if (!plan) {
763
+ const result = await planOrphanAdoptions(ctx, {
764
+ limit: ctx.config.maintenance.adopt.maxPages,
765
+ });
766
+ report.adopted = result.adopted;
767
+ if (result.drafts.length > 0) {
768
+ plan = createAdoptionPlan(ctx, mode, result.drafts, report.run.snapshot, adoptPolicy);
769
+ }
757
770
  }
758
771
  if (!plan)
759
772
  return null;
@@ -1151,12 +1164,16 @@ async function observePhase(ctx, options, report, applied, budget, deferAutomati
1151
1164
  if (plan && !planMatchesPolicies(plan, { observe: observePolicy }))
1152
1165
  plan = null;
1153
1166
  const previouslyRejected = [];
1167
+ const scopeHeld = [];
1154
1168
  if (!plan) {
1155
1169
  const prepared = await collectPreparedObservations(ctx, report);
1156
1170
  const drafts = [];
1157
1171
  for (const entry of prepared) {
1158
- if (!entry.draft)
1172
+ if (!entry.draft) {
1173
+ if (entry.rejectionCode)
1174
+ scopeHeld.push({ ...entry.written, action: 'rejected' });
1159
1175
  continue;
1176
+ }
1160
1177
  if (inferenceWasRejected(ctx, 'observe', entry.draft)) {
1161
1178
  previouslyRejected.push({ ...entry.written, action: 'rejected' });
1162
1179
  }
@@ -1167,7 +1184,7 @@ async function observePhase(ctx, options, report, applied, budget, deferAutomati
1167
1184
  plan = createObservationPlan(ctx, observePolicy, drafts, observePolicy);
1168
1185
  }
1169
1186
  if (!plan) {
1170
- report.observations.push(...previouslyRejected);
1187
+ report.observations.push(...scopeHeld, ...previouslyRejected);
1171
1188
  return;
1172
1189
  }
1173
1190
  if (observePolicy === 'auto' && !deferAutomaticApply) {
@@ -1180,7 +1197,7 @@ async function observePhase(ctx, options, report, applied, budget, deferAutomati
1180
1197
  applied.push(...result.files.map((file) => asApplied('observe', file)));
1181
1198
  }
1182
1199
  }
1183
- report.observations.push(...plan.items.map(observationFromPlanItem), ...previouslyRejected);
1200
+ report.observations.push(...plan.items.map(observationFromPlanItem), ...scopeHeld, ...previouslyRejected);
1184
1201
  const changeIds = plan.items
1185
1202
  .map((item) => item.changeId)
1186
1203
  .filter((changeId) => changeId !== null);
@@ -1214,7 +1231,11 @@ async function collectPreparedObservations(ctx, report) {
1214
1231
  const prepared = await staleObservationOutcomes(ctx);
1215
1232
  for (const entry of prepared) {
1216
1233
  if (entry.rejectionReason)
1217
- report.rejected.push({ pattern: entry.written.pattern, reason: entry.rejectionReason });
1234
+ report.rejected.push({
1235
+ pattern: entry.written.pattern,
1236
+ reason: entry.rejectionReason,
1237
+ ...(entry.rejectionCode ? { code: entry.rejectionCode } : {}),
1238
+ });
1218
1239
  }
1219
1240
  if (groups.length === 0)
1220
1241
  return prepared;
@@ -1223,6 +1244,10 @@ async function collectPreparedObservations(ctx, report) {
1223
1244
  const knownFacts = liveFactClaims(ctx, report.conflicts);
1224
1245
  const observationsBySlug = indexedObservations(ctx);
1225
1246
  for (const group of groups) {
1247
+ if (!group.scopeContextComplete) {
1248
+ report.warnings.push(`observe (${group.subject}): scope_context_incomplete — complete current evidence exceeded the bounded assessment`);
1249
+ continue;
1250
+ }
1226
1251
  const result = await runObserveMission({
1227
1252
  // The display label comes from the canonical entity page. Group membership itself is the
1228
1253
  // exact resolved entity id below, never a folder or a model-generated subject string.
@@ -1248,21 +1273,76 @@ async function collectPreparedObservations(ctx, report) {
1248
1273
  }
1249
1274
  report.rejected.push(...result.rejected);
1250
1275
  for (const observation of result.observations) {
1251
- const outcome = await prepareCoLocatedObservation(ctx, group, observation);
1276
+ const scopeCandidate = exactObservationCandidateForScope(ctx, observation);
1277
+ const scope = await assessObservationScope({
1278
+ store: ctx.store,
1279
+ model: ctx.models.derive,
1280
+ level: 'observation',
1281
+ subject: group.subject,
1282
+ candidate: scopeCandidate,
1283
+ evidence: group.scopeFacts
1284
+ .map((fact) => ({
1285
+ ...fact,
1286
+ status: observation.evidence.includes(fact.id) ? 'selected_support' : fact.status,
1287
+ }))
1288
+ .sort((a, b) => `${a.slug}\0${a.id}`.localeCompare(`${b.slug}\0${b.id}`)),
1289
+ contextComplete: group.scopeContextComplete,
1290
+ });
1291
+ if (!scope.candidate || !scope.receipt) {
1292
+ const held = {
1293
+ written: {
1294
+ slug: group.targetSlug,
1295
+ pattern: observation.pattern,
1296
+ evidence: [],
1297
+ action: 'rejected',
1298
+ },
1299
+ draft: null,
1300
+ rejectionReason: scope.hold?.reason ?? 'the evidence-scope assessment held the candidate',
1301
+ ...(scope.hold ? { rejectionCode: scope.hold.code } : {}),
1302
+ };
1303
+ prepared.push(held);
1304
+ report.rejected.push({
1305
+ pattern: observation.pattern,
1306
+ reason: held.rejectionReason,
1307
+ ...(held.rejectionCode ? { code: held.rejectionCode } : {}),
1308
+ });
1309
+ continue;
1310
+ }
1311
+ const assessed = scope.candidate;
1312
+ const outcome = await prepareCoLocatedObservation(ctx, group, assessed, scope.receipt);
1252
1313
  prepared.push(outcome);
1253
1314
  if (outcome.rejectionReason) {
1254
- report.rejected.push({ pattern: observation.pattern, reason: outcome.rejectionReason });
1315
+ report.rejected.push({
1316
+ pattern: assessed.pattern,
1317
+ reason: outcome.rejectionReason,
1318
+ ...(outcome.rejectionCode ? { code: outcome.rejectionCode } : {}),
1319
+ });
1255
1320
  }
1256
1321
  // So a later group in this same run sees it. Without this, cross-page duplicates are caught
1257
1322
  // only from the second night onwards — the night they are created, they both go through.
1258
1323
  observationsBySlug.set(group.targetSlug, [
1259
1324
  ...(observationsBySlug.get(group.targetSlug) ?? []),
1260
- observation.pattern,
1325
+ assessed.pattern,
1261
1326
  ]);
1262
1327
  }
1263
1328
  }
1264
1329
  return prepared;
1265
1330
  }
1331
+ function exactObservationCandidateForScope(ctx, candidate) {
1332
+ if (candidate.outcome !== 'reinforce' || !candidate.targetId)
1333
+ return candidate;
1334
+ const row = ctx.store.db
1335
+ .prepare(`SELECT payload FROM observation_entries
1336
+ WHERE id = ? AND disposition = 'active' AND eligible = 1`)
1337
+ .get(candidate.targetId);
1338
+ if (!row)
1339
+ return candidate;
1340
+ const pattern = row.payload
1341
+ .replace(/^- \*\*Observation:\*\*\s*/, '')
1342
+ .replace(/\s+Evidence:\s+(?:\[\[[^\]]+\]\]\s*)+$/, '')
1343
+ .trim();
1344
+ return pattern ? { ...candidate, pattern } : candidate;
1345
+ }
1266
1346
  async function staleObservationOutcomes(ctx) {
1267
1347
  const rows = ctx.store.db
1268
1348
  .prepare(`SELECT oe.id, oe.source_slug, oe.payload, oe.payload_hash, oe.subject_entity,
@@ -1360,10 +1440,11 @@ async function staleObservationOutcomes(ctx) {
1360
1440
  }
1361
1441
  /** Live eligible level-one facts grouped only by exact resolved subject identity. */
1362
1442
  function subjectGroups(ctx, maxSubjects, conflicts) {
1363
- const ineligible = ineligibleConflictClaims(conflicts);
1443
+ const conflictStatus = conflictClaimIneligibility(conflicts);
1364
1444
  const rows = ctx.store.db
1365
1445
  .prepare(`SELECT f.id, f.claim, f.line_start, f.source_line_hash, f.item_id,
1366
1446
  p.id AS page_id, p.slug, p.updated_at, g.subject_entity,
1447
+ g.eligibility, g.traversable,
1367
1448
  e.canonical_page, cp.slug AS canonical_slug, cp.rel_path AS canonical_rel_path,
1368
1449
  cp.observe_management AS canonical_observe
1369
1450
  FROM facts f JOIN pages p ON p.id = f.page_id
@@ -1374,52 +1455,62 @@ function subjectGroups(ctx, maxSubjects, conflicts) {
1374
1455
  AND f.confidence >= 0.5
1375
1456
  AND p.role = 'knowledge'
1376
1457
  AND p.derived_hash = p.body_hash
1377
- AND g.eligibility = 'eligible'
1378
- AND g.traversable = 1
1379
- ORDER BY p.updated_at DESC`)
1458
+ ORDER BY p.updated_at DESC, p.slug, f.line_start, f.id`)
1380
1459
  .all();
1381
1460
  const groups = new Map();
1382
1461
  for (const row of rows) {
1383
- if (ineligible.has(claimKey(row.slug, row.line_start)))
1384
- continue;
1385
1462
  const target = observationTarget(ctx, row);
1386
1463
  if (!target)
1387
1464
  continue;
1388
1465
  const proofs = [...proofGroupsForFact(ctx.store, row.id, row.page_id, row.item_id)].sort();
1389
- if (proofs.length === 0)
1390
- continue;
1391
1466
  const key = row.subject_entity;
1392
- const existing = groups.get(key);
1393
- if (existing) {
1394
- if (existing.targetSlug !== target.slug) {
1467
+ const conflictReason = conflictStatus.get(claimKey(row.slug, row.line_start));
1468
+ const canSupport = !conflictReason && row.eligibility === 'eligible' && row.traversable === 1 && proofs.length > 0;
1469
+ let group = groups.get(key);
1470
+ if (group) {
1471
+ if (group.targetSlug !== target.slug) {
1395
1472
  groups.delete(key);
1396
1473
  continue;
1397
1474
  }
1398
- if (existing.facts.length < 30) {
1399
- existing.facts.push({
1400
- id: row.id,
1401
- claim: row.claim,
1402
- slug: row.slug,
1403
- sourceLineHash: row.source_line_hash,
1404
- proofGroups: proofs,
1405
- });
1406
- }
1407
1475
  }
1408
1476
  else {
1409
- groups.set(key, {
1477
+ group = {
1410
1478
  subject: target.title,
1411
1479
  subjectEntity: row.subject_entity,
1412
1480
  targetSlug: target.slug,
1413
1481
  targetRelPath: target.relPath,
1414
- facts: [
1415
- {
1416
- id: row.id,
1417
- claim: row.claim,
1418
- slug: row.slug,
1419
- sourceLineHash: row.source_line_hash,
1420
- proofGroups: proofs,
1421
- },
1422
- ],
1482
+ facts: [],
1483
+ scopeFacts: [],
1484
+ scopeContextComplete: true,
1485
+ scopeCharacters: 0,
1486
+ };
1487
+ groups.set(key, group);
1488
+ }
1489
+ const scopeCharacters = row.id.length + row.claim.length + row.slug.length;
1490
+ if (group.scopeFacts.length >= 80 || group.scopeCharacters + scopeCharacters > 80_000) {
1491
+ group.scopeContextComplete = false;
1492
+ }
1493
+ else {
1494
+ group.scopeFacts.push({
1495
+ id: row.id,
1496
+ claim: row.claim,
1497
+ slug: row.slug,
1498
+ status: conflictReason ? 'counterevidence' : canSupport ? 'eligible_context' : 'ineligible_context',
1499
+ ...(!canSupport
1500
+ ? {
1501
+ statusReason: conflictReason ?? `graph_${row.eligibility}_${row.traversable ? 'linked' : 'held'}`,
1502
+ }
1503
+ : {}),
1504
+ });
1505
+ group.scopeCharacters += scopeCharacters;
1506
+ }
1507
+ if (canSupport && group.facts.length < 30) {
1508
+ group.facts.push({
1509
+ id: row.id,
1510
+ claim: row.claim,
1511
+ slug: row.slug,
1512
+ sourceLineHash: row.source_line_hash,
1513
+ proofGroups: proofs,
1423
1514
  });
1424
1515
  }
1425
1516
  }
@@ -1477,7 +1568,7 @@ function indexedObservationRecords(ctx, subject, slug) {
1477
1568
  .trim(),
1478
1569
  }));
1479
1570
  }
1480
- async function prepareCoLocatedObservation(ctx, group, observation) {
1571
+ async function prepareCoLocatedObservation(ctx, group, observation, scopeReceipt) {
1481
1572
  let selected = observation.evidence.flatMap((id) => {
1482
1573
  const fact = group.facts.find((candidate) => candidate.id === id);
1483
1574
  return fact ? [fact] : [];
@@ -1572,6 +1663,7 @@ async function prepareCoLocatedObservation(ctx, group, observation) {
1572
1663
  disposition: 'active',
1573
1664
  evidence: locators,
1574
1665
  proofCount,
1666
+ scopeAssessment: scopeReceipt.fingerprint,
1575
1667
  };
1576
1668
  const block = observationBlock(marker, finalPattern, selected.map((fact) => fact.slug));
1577
1669
  if (outcome === 'create' && observationMarkerIndexes(before.split(/\r?\n/), marker.id, true).length > 0) {
@@ -1636,6 +1728,12 @@ async function prepareCoLocatedObservation(ctx, group, observation) {
1636
1728
  observationDisposition: marker.disposition,
1637
1729
  observationProofCount: marker.proofCount,
1638
1730
  observationOutcome: outcome,
1731
+ observationScopeAssessment: scopeReceipt.fingerprint,
1732
+ observationScopeCandidateHash: scopeReceipt.candidateHash,
1733
+ observationScopeContextHash: scopeReceipt.contextHash,
1734
+ observationScopePromptVersion: scopeReceipt.promptVersion,
1735
+ observationScopeModel: scopeReceipt.modelId,
1736
+ observationScopeNarrowed: scopeReceipt.narrowed,
1639
1737
  ...(targetMarker ? { observationTargetId: targetMarker.id } : {}),
1640
1738
  },
1641
1739
  };
@@ -1852,12 +1950,16 @@ async function reflectPhase(ctx, options, report, applied, budget, deferAutomati
1852
1950
  if (plan && !planMatchesPolicies(plan, { reflect: reflectPolicy }))
1853
1951
  plan = null;
1854
1952
  const previouslyRejected = [];
1953
+ const scopeHeld = [];
1855
1954
  if (!plan) {
1856
1955
  const prepared = await collectPreparedReflections(ctx, report);
1857
1956
  const drafts = [];
1858
1957
  for (const entry of prepared) {
1859
- if (!entry.draft)
1958
+ if (!entry.draft) {
1959
+ if (entry.rejectionCode)
1960
+ scopeHeld.push({ ...entry.written, action: 'rejected' });
1860
1961
  continue;
1962
+ }
1861
1963
  if (inferenceWasRejected(ctx, 'reflect', entry.draft)) {
1862
1964
  previouslyRejected.push({ ...entry.written, action: 'rejected' });
1863
1965
  }
@@ -1868,7 +1970,7 @@ async function reflectPhase(ctx, options, report, applied, budget, deferAutomati
1868
1970
  plan = createReflectionPlan(ctx, reflectPolicy, drafts, reflectPolicy);
1869
1971
  }
1870
1972
  if (!plan) {
1871
- report.observations.push(...previouslyRejected);
1973
+ report.observations.push(...scopeHeld, ...previouslyRejected);
1872
1974
  return;
1873
1975
  }
1874
1976
  if (reflectPolicy === 'auto' && !deferAutomaticApply) {
@@ -1881,7 +1983,7 @@ async function reflectPhase(ctx, options, report, applied, budget, deferAutomati
1881
1983
  applied.push(...result.files.map((file) => asApplied('reflect', file)));
1882
1984
  }
1883
1985
  }
1884
- report.observations.push(...plan.items.map(observationFromPlanItem), ...previouslyRejected);
1986
+ report.observations.push(...plan.items.map(observationFromPlanItem), ...scopeHeld, ...previouslyRejected);
1885
1987
  const changeIds = plan.items
1886
1988
  .map((item) => item.changeId)
1887
1989
  .filter((changeId) => changeId !== null);
@@ -1903,15 +2005,25 @@ async function collectPreparedReflections(ctx, report) {
1903
2005
  .prepare(`SELECT oe.id, oe.source_slug AS slug, oe.payload, oe.payload_hash, p.rel_path
1904
2006
  FROM observation_entries oe JOIN pages p ON p.id = oe.source_page
1905
2007
  WHERE oe.eligible = 1 AND oe.disposition = 'active'
1906
- ORDER BY p.updated_at DESC, oe.marker_line DESC LIMIT 40`)
2008
+ ORDER BY p.updated_at DESC, oe.source_slug, oe.marker_line DESC, oe.id LIMIT 41`)
1907
2009
  .all();
1908
2010
  if (rows.length < 2) {
1909
2011
  report.warnings.push('reflect had fewer than two observations to build on — nothing was written');
1910
2012
  return [];
1911
2013
  }
2014
+ if (rows.length > 40) {
2015
+ report.warnings.push('reflect: scope_context_incomplete — more than 40 eligible observations require a broader assessment');
2016
+ return [];
2017
+ }
2018
+ const boundedRows = rows;
2019
+ const scopeBase = reflectionScopeEvidence(ctx, boundedRows);
2020
+ if (!scopeBase.complete) {
2021
+ report.warnings.push('reflect: scope_context_incomplete — complete current leaf evidence exceeded the bounded assessment');
2022
+ return [];
2023
+ }
1912
2024
  const result = await runObserveMission({
1913
2025
  subject: 'decision principles',
1914
- facts: rows.map((row) => ({ id: row.id, claim: row.payload, slug: row.slug })),
2026
+ facts: boundedRows.map((row) => ({ id: row.id, claim: row.payload, slug: row.slug })),
1915
2027
  model: ctx.models.derive,
1916
2028
  mission: ctx.config.maintenance.reflect.mission ??
1917
2029
  'State durable decision principles and long-term tendencies, not individual patterns.',
@@ -1934,10 +2046,49 @@ async function collectPreparedReflections(ctx, report) {
1934
2046
  report.rejected.push(...result.rejected);
1935
2047
  const prepared = [];
1936
2048
  for (const observation of result.observations) {
1937
- const outcome = await prepareIndexedReflection(ctx, observation, rows);
2049
+ const scopeEvidence = scopeBase.evidence.map((entry) => ({
2050
+ ...entry,
2051
+ status: observation.evidence.includes(entry.id)
2052
+ ? 'selected_support'
2053
+ : 'eligible_context',
2054
+ }));
2055
+ const scope = await assessObservationScope({
2056
+ store: ctx.store,
2057
+ model: ctx.models.derive,
2058
+ level: 'reflection',
2059
+ subject: 'decision principles',
2060
+ candidate: observation,
2061
+ evidence: scopeEvidence.sort((a, b) => `${a.slug}\0${a.id}`.localeCompare(`${b.slug}\0${b.id}`)),
2062
+ contextComplete: true,
2063
+ });
2064
+ if (!scope.candidate || !scope.receipt) {
2065
+ const held = {
2066
+ written: {
2067
+ slug: target,
2068
+ pattern: observation.pattern,
2069
+ evidence: [],
2070
+ action: 'rejected',
2071
+ },
2072
+ draft: null,
2073
+ rejectionReason: scope.hold?.reason ?? 'the evidence-scope assessment held the principle',
2074
+ ...(scope.hold ? { rejectionCode: scope.hold.code } : {}),
2075
+ };
2076
+ prepared.push(held);
2077
+ report.rejected.push({
2078
+ pattern: observation.pattern,
2079
+ reason: held.rejectionReason,
2080
+ ...(held.rejectionCode ? { code: held.rejectionCode } : {}),
2081
+ });
2082
+ continue;
2083
+ }
2084
+ const outcome = await prepareIndexedReflection(ctx, scope.candidate, boundedRows, scope.receipt);
1938
2085
  prepared.push(outcome);
1939
2086
  if (outcome.rejectionReason) {
1940
- report.rejected.push({ pattern: observation.pattern, reason: outcome.rejectionReason });
2087
+ report.rejected.push({
2088
+ pattern: scope.candidate.pattern,
2089
+ reason: outcome.rejectionReason,
2090
+ ...(outcome.rejectionCode ? { code: outcome.rejectionCode } : {}),
2091
+ });
1941
2092
  }
1942
2093
  }
1943
2094
  return prepared;
@@ -1957,7 +2108,45 @@ async function recordedPrinciples(ctx) {
1957
2108
  .trim())
1958
2109
  .filter(Boolean);
1959
2110
  }
1960
- async function prepareIndexedReflection(ctx, observation, rows) {
2111
+ function reflectionScopeEvidence(ctx, rows) {
2112
+ const evidence = [];
2113
+ let complete = true;
2114
+ let leafCount = 0;
2115
+ let characters = 0;
2116
+ for (const row of rows) {
2117
+ const leaves = ctx.store.db
2118
+ .prepare(`SELECT f.id, f.claim, p.slug
2119
+ FROM observation_evidence oe JOIN facts f ON f.id = oe.fact_id
2120
+ JOIN pages p ON p.id = f.page_id
2121
+ WHERE oe.observation_id = ? AND f.valid_to IS NULL
2122
+ ORDER BY oe.ordinal`)
2123
+ .all(row.id);
2124
+ const expected = ctx.store.db
2125
+ .prepare('SELECT count(*) AS count FROM observation_evidence WHERE observation_id = ?')
2126
+ .get(row.id);
2127
+ if (leaves.length !== expected.count)
2128
+ complete = false;
2129
+ const nextCharacters = row.id.length +
2130
+ row.slug.length +
2131
+ row.payload.length +
2132
+ leaves.reduce((sum, leaf) => sum + leaf.id.length + leaf.slug.length + leaf.claim.length, 0);
2133
+ if (leafCount + leaves.length > 120 || characters + nextCharacters > 80_000) {
2134
+ complete = false;
2135
+ continue;
2136
+ }
2137
+ evidence.push({
2138
+ id: row.id,
2139
+ claim: row.payload,
2140
+ slug: row.slug,
2141
+ status: 'eligible_context',
2142
+ leaves,
2143
+ });
2144
+ leafCount += leaves.length;
2145
+ characters += nextCharacters;
2146
+ }
2147
+ return { evidence, complete };
2148
+ }
2149
+ async function prepareIndexedReflection(ctx, observation, rows, scopeReceipt) {
1961
2150
  const selected = observation.evidence.flatMap((id) => {
1962
2151
  const row = rows.find((candidate) => candidate.id === id);
1963
2152
  return row ? [row] : [];
@@ -2020,8 +2209,25 @@ async function prepareIndexedReflection(ctx, observation, rows) {
2020
2209
  before: before === null ? null : sha256(before),
2021
2210
  pattern: observation.pattern,
2022
2211
  evidence,
2212
+ scopeAssessment: scopeReceipt,
2023
2213
  }));
2024
- return { written, draft: { slug, relPath, inputHash, before, after, evidence } };
2214
+ return {
2215
+ written,
2216
+ draft: {
2217
+ slug,
2218
+ relPath,
2219
+ inputHash,
2220
+ before,
2221
+ after,
2222
+ evidence,
2223
+ observationScopeAssessment: scopeReceipt.fingerprint,
2224
+ observationScopeCandidateHash: scopeReceipt.candidateHash,
2225
+ observationScopeContextHash: scopeReceipt.contextHash,
2226
+ observationScopePromptVersion: scopeReceipt.promptVersion,
2227
+ observationScopeModel: scopeReceipt.modelId,
2228
+ observationScopeNarrowed: scopeReceipt.narrowed,
2229
+ },
2230
+ };
2025
2231
  }
2026
2232
  /** A journal entry, as the log wants it: which phase, and what the write added. */
2027
2233
  function asApplied(phase, file) {