@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
@@ -19,6 +19,7 @@ import { hasInlineMergeConflict, matchesConflictPath, quarantineReasonsForPath,
19
19
  import { activeDreamRuns, getDreamRun, latestDreamRun, latestFullDreamRun, listDreamRuns, } from "./runs.js";
20
20
  import { declaringRule, effectiveRule } from "../rules/compile.js";
21
21
  import { configuredMaintenanceAuthority } from "./profile.js";
22
+ import { OBSERVATION_SCOPE_PROMPT_VERSION, observationScopeCandidateHash, observationScopeContextHash, } from "./observation-scope.js";
22
23
  import { createMaintenanceBudget, maintenanceBudgetReceipt, reserveMaintenanceBudget, } from "./budget.js";
23
24
  import { managedItemOperationsIssue, } from "./managed-items.js";
24
25
  import { rewritePageLinks, rewriteRelocatedPageReferences } from "./rule-drift.js";
@@ -164,6 +165,22 @@ function createInferencePlan(ctx, mode, kind, drafts, policy) {
164
165
  ...(draft.observationQualificationIssue
165
166
  ? { observationQualificationIssue: draft.observationQualificationIssue }
166
167
  : {}),
168
+ ...(draft.observationScopeAssessment
169
+ ? { observationScopeAssessment: draft.observationScopeAssessment }
170
+ : {}),
171
+ ...(draft.observationScopeCandidateHash
172
+ ? { observationScopeCandidateHash: draft.observationScopeCandidateHash }
173
+ : {}),
174
+ ...(draft.observationScopeContextHash
175
+ ? { observationScopeContextHash: draft.observationScopeContextHash }
176
+ : {}),
177
+ ...(draft.observationScopePromptVersion
178
+ ? { observationScopePromptVersion: draft.observationScopePromptVersion }
179
+ : {}),
180
+ ...(draft.observationScopeModel ? { observationScopeModel: draft.observationScopeModel } : {}),
181
+ ...(draft.observationScopeNarrowed !== undefined
182
+ ? { observationScopeNarrowed: draft.observationScopeNarrowed }
183
+ : {}),
167
184
  ...(entry.reflectionObservationId ? { reflectionObservationId: entry.reflectionObservationId } : {}),
168
185
  ...(entry.reflectionPayloadHash ? { reflectionPayloadHash: entry.reflectionPayloadHash } : {}),
169
186
  })),
@@ -175,6 +192,9 @@ function createInferencePlan(ctx, mode, kind, drafts, policy) {
175
192
  : 'at least three distinct live observation sources',
176
193
  status: 'passed',
177
194
  },
195
+ ...(draft.observationScopeAssessment
196
+ ? [{ name: 'exact candidate passed separate evidence-scope assessment', status: 'passed' }]
197
+ : []),
178
198
  {
179
199
  name: kind === 'observe' ? 'marker-owned observation block shape' : 'append-only derived principle shape',
180
200
  status: 'passed',
@@ -1425,11 +1445,22 @@ function intersects(left, right) {
1425
1445
  return true;
1426
1446
  return false;
1427
1447
  }
1428
- /** Oldest unfinished plan for restart recovery; fresh planning waits until it is resolved. */
1448
+ /**
1449
+ * Oldest plan with work that can actually progress after restart.
1450
+ *
1451
+ * Older versions could leave a mixed plan as `partially_completed` after its only remaining
1452
+ * items became blocked, stale, or verification-failed. Plan status alone therefore cannot decide
1453
+ * whether a plan is resumable: selecting that residual forever would starve independent work.
1454
+ */
1429
1455
  export function findActiveMaintenancePlan(ctx, mode, phase) {
1430
1456
  const row = ctx.store.db
1431
1457
  .prepare(`SELECT id FROM maintenance_plans WHERE mode = ? AND phase = ?
1432
1458
  AND status NOT IN ('completed', 'failed', 'superseded')
1459
+ AND EXISTS (
1460
+ SELECT 1 FROM maintenance_items item
1461
+ WHERE item.plan_id = maintenance_plans.id
1462
+ AND item.status IN ('proposed', 'approved', 'applying', 'verification_pending')
1463
+ )
1433
1464
  ORDER BY rowid LIMIT 1`)
1434
1465
  .get(mode, phase);
1435
1466
  return row ? getMaintenancePlan(ctx, row.id) : null;
@@ -2746,10 +2777,24 @@ async function indexMaintenanceItem(ctx, item, documentState = 'after') {
2746
2777
  await Promise.all(paths.map((relPath) => safeOperationPath(ctx, relPath)));
2747
2778
  alignRelocatedPageRow(ctx, item, documentState);
2748
2779
  alignMovedDocumentRows(ctx, item.operations, documentState);
2780
+ const preserveDerivedViews = item.kind === 'observe' &&
2781
+ item.evidence.some((entry) => entry.source === item.subject) &&
2782
+ observationOperationIssue(ctx, item, item.operations) === null
2783
+ ? item.operations.flatMap((operation) => operation.type === 'replace'
2784
+ ? [
2785
+ {
2786
+ relPath: operation.relPath,
2787
+ fromBodyHash: parsePage(operation.relPath, documentState === 'after' ? operation.before : operation.after).bodyHash,
2788
+ toBodyHash: parsePage(operation.relPath, documentState === 'after' ? operation.after : operation.before).bodyHash,
2789
+ },
2790
+ ]
2791
+ : [])
2792
+ : [];
2749
2793
  await ctx.indexer.run({
2750
2794
  only: paths,
2751
2795
  modelPaths: [],
2752
2796
  ...(item.kind === 'adopt' ? { reindexUnchanged: true } : {}),
2797
+ ...(preserveDerivedViews.length > 0 ? { preserveDerivedViews } : {}),
2753
2798
  });
2754
2799
  }
2755
2800
  /** Restore a maintenance-relocated page's derived identity before generic undo reindexes the tree. */
@@ -3085,6 +3130,24 @@ function observationOperationIssue(ctx, item, operations) {
3085
3130
  return 'a reflection item must write only the configured observations/principles page';
3086
3131
  }
3087
3132
  const sources = item.evidence.filter((entry) => entry.type === 'page');
3133
+ const firstScope = sources[0];
3134
+ if (item.kind === 'reflect' &&
3135
+ (!firstScope?.observationScopeAssessment ||
3136
+ !/^[a-f0-9]{64}$/.test(firstScope.observationScopeAssessment) ||
3137
+ !firstScope.observationScopeCandidateHash ||
3138
+ !/^[a-f0-9]{64}$/.test(firstScope.observationScopeCandidateHash) ||
3139
+ !firstScope.observationScopeContextHash ||
3140
+ !/^[a-f0-9]{64}$/.test(firstScope.observationScopeContextHash) ||
3141
+ firstScope.observationScopePromptVersion !== OBSERVATION_SCOPE_PROMPT_VERSION ||
3142
+ !firstScope.observationScopeModel ||
3143
+ sources.some((entry) => entry.observationScopeAssessment !== firstScope.observationScopeAssessment ||
3144
+ entry.observationScopeCandidateHash !== firstScope.observationScopeCandidateHash ||
3145
+ entry.observationScopeContextHash !== firstScope.observationScopeContextHash ||
3146
+ entry.observationScopePromptVersion !== firstScope.observationScopePromptVersion ||
3147
+ entry.observationScopeModel !== firstScope.observationScopeModel ||
3148
+ entry.observationScopeNarrowed !== firstScope.observationScopeNarrowed))) {
3149
+ return 'a reflection requires one exact current evidence-scope assessment';
3150
+ }
3088
3151
  const minEvidence = item.kind === 'reflect'
3089
3152
  ? Math.max(3, ctx.config.maintenance.observe.minEvidence)
3090
3153
  : ctx.config.maintenance.observe.minEvidence;
@@ -3121,6 +3184,10 @@ function observationOperationIssue(ctx, item, operations) {
3121
3184
  if (!match || match[2].trim().length === 0) {
3122
3185
  return 'the inference item must append one dated, cited conclusion line';
3123
3186
  }
3187
+ if (item.kind === 'reflect' &&
3188
+ observationScopeCandidateHash({ pattern: match[2].trim() }) !== firstScope?.observationScopeCandidateHash) {
3189
+ return 'the reflected principle changed after its evidence-scope assessment';
3190
+ }
3124
3191
  const citations = [...match[3].matchAll(/\[\[([^\]|#]+)(?:[#|][^\]]*)?\]\]/g)].map((entry) => entry[1]);
3125
3192
  if (!sameStringSet(citations, sourceSlugs)) {
3126
3193
  return 'the appended inference line must cite exactly its sealed evidence pages';
@@ -3198,6 +3265,23 @@ function coLocatedObservationOperationIssue(ctx, item, operations) {
3198
3265
  entry.observationProofCount !== first.observationProofCount)) {
3199
3266
  return 'observation evidence disagrees about marker identity';
3200
3267
  }
3268
+ if (!closesInvalidLineage &&
3269
+ (!first.observationScopeAssessment ||
3270
+ !/^[a-f0-9]{64}$/.test(first.observationScopeAssessment) ||
3271
+ !first.observationScopeCandidateHash ||
3272
+ !/^[a-f0-9]{64}$/.test(first.observationScopeCandidateHash) ||
3273
+ !first.observationScopeContextHash ||
3274
+ !/^[a-f0-9]{64}$/.test(first.observationScopeContextHash) ||
3275
+ first.observationScopePromptVersion !== OBSERVATION_SCOPE_PROMPT_VERSION ||
3276
+ !first.observationScopeModel ||
3277
+ sources.some((entry) => entry.observationScopeAssessment !== first.observationScopeAssessment ||
3278
+ entry.observationScopeCandidateHash !== first.observationScopeCandidateHash ||
3279
+ entry.observationScopeContextHash !== first.observationScopeContextHash ||
3280
+ entry.observationScopePromptVersion !== first.observationScopePromptVersion ||
3281
+ entry.observationScopeModel !== first.observationScopeModel ||
3282
+ entry.observationScopeNarrowed !== first.observationScopeNarrowed))) {
3283
+ return 'an observation requires one exact current evidence-scope assessment';
3284
+ }
3201
3285
  const factIds = sources.map((entry) => entry.observationFactId);
3202
3286
  if (new Set(factIds).size !== factIds.length)
3203
3287
  return 'observation evidence contains a duplicate fact';
@@ -3214,7 +3298,11 @@ function coLocatedObservationOperationIssue(ctx, item, operations) {
3214
3298
  return 'the observation marker is missing from the proposed page';
3215
3299
  const marker = parseObservationMarker(afterPage.lines[markerIndex]);
3216
3300
  const payload = afterPage.lines[markerIndex + 1]?.trim() ?? '';
3217
- if (!marker || marker.subject !== first.observationSubject) {
3301
+ if (!marker ||
3302
+ marker.subject !== first.observationSubject ||
3303
+ (!closesInvalidLineage &&
3304
+ outcome !== 'split' &&
3305
+ marker.scopeAssessment !== first.observationScopeAssessment)) {
3218
3306
  return 'the proposed observation marker does not match its sealed lineage';
3219
3307
  }
3220
3308
  if (closesInvalidLineage) {
@@ -3262,6 +3350,8 @@ function coLocatedObservationOperationIssue(ctx, item, operations) {
3262
3350
  const patterns = activePayloads.map((entry) => /^- \*\*Observation:\*\* (.+?) Evidence: /.exec(entry)?.[1]?.trim() ?? '');
3263
3351
  const activeIds = activeMarkers.map((entry) => entry.id);
3264
3352
  if (patterns.some((entry) => !entry) ||
3353
+ observationScopeCandidateHash({ pattern: patterns[0], splitPattern: patterns[1] }) !==
3354
+ first.observationScopeCandidateHash ||
3265
3355
  activePayloads.some((entry) => observationPayloadIssue(entry, evidenceSlugs) !== null) ||
3266
3356
  activeMarkers.some((entry) => renderObservationMarker(entry) !==
3267
3357
  renderObservationMarker({
@@ -3270,6 +3360,7 @@ function coLocatedObservationOperationIssue(ctx, item, operations) {
3270
3360
  disposition: 'active',
3271
3361
  evidence: expectedEvidence,
3272
3362
  proofCount: first.observationProofCount,
3363
+ scopeAssessment: first.observationScopeAssessment,
3273
3364
  })) ||
3274
3365
  new Set(activeIds).size !== 2 ||
3275
3366
  activeIds.includes(targetId) ||
@@ -3293,6 +3384,9 @@ function coLocatedObservationOperationIssue(ctx, item, operations) {
3293
3384
  const pattern = /^- \*\*Observation:\*\* (.+?) Evidence: /.exec(payload)?.[1]?.trim();
3294
3385
  if (!pattern)
3295
3386
  return 'the proposed observation lacks visible derived wording';
3387
+ if (observationScopeCandidateHash({ pattern }) !== first.observationScopeCandidateHash) {
3388
+ return 'the observation wording changed after its evidence-scope assessment';
3389
+ }
3296
3390
  const expectedMarker = {
3297
3391
  ...marker,
3298
3392
  evidence: sources.map((entry) => ({
@@ -3398,10 +3492,23 @@ async function observationEvidenceIssue(ctx, item, phase = 'before') {
3398
3492
  const bytes = await fsp
3399
3493
  .readFile(path.join(ctx.config.aknoPath, fact.rel_path), 'utf8')
3400
3494
  .catch(() => null);
3401
- if (bytes === null || sha256(bytes) !== entry.fingerprint) {
3495
+ const coLocatedOperation = phase === 'after' && entry.source === item.subject
3496
+ ? item.operations.find((operation) => operation.type === 'replace' &&
3497
+ operation.relPath === fact.rel_path &&
3498
+ sha256(operation.before) === entry.fingerprint)
3499
+ : undefined;
3500
+ if (bytes === null ||
3501
+ (coLocatedOperation
3502
+ ? sha256(bytes) !== coLocatedOperation.afterHash
3503
+ : sha256(bytes) !== entry.fingerprint)) {
3402
3504
  return `${entry.source} no longer matches its sealed observation evidence.`;
3403
3505
  }
3404
3506
  }
3507
+ if (item.policy === 'review') {
3508
+ const scopeIssue = currentObservationScopeIssue(ctx, item);
3509
+ if (scopeIssue)
3510
+ return scopeIssue;
3511
+ }
3405
3512
  if (phase === 'after') {
3406
3513
  const operation = item.operations[0];
3407
3514
  if (!operation || operation.type !== 'replace')
@@ -3459,6 +3566,11 @@ async function observationEvidenceIssue(ctx, item, phase = 'before') {
3459
3566
  return `${entry.source} no longer matches its sealed reflection evidence.`;
3460
3567
  }
3461
3568
  }
3569
+ if (item.policy === 'review') {
3570
+ const scopeIssue = currentObservationScopeIssue(ctx, item);
3571
+ if (scopeIssue)
3572
+ return scopeIssue;
3573
+ }
3462
3574
  return null;
3463
3575
  }
3464
3576
  const expectedRole = item.kind === 'reflect' ? 'inference' : 'knowledge';
@@ -3487,6 +3599,114 @@ async function observationEvidenceIssue(ctx, item, phase = 'before') {
3487
3599
  }
3488
3600
  return null;
3489
3601
  }
3602
+ /** Rebuild the complete bounded assessment input so delayed review plans cannot outlive context changes. */
3603
+ function currentObservationScopeIssue(ctx, item) {
3604
+ const expected = item.evidence[0]?.observationScopeContextHash;
3605
+ if (!expected)
3606
+ return 'the inference plan is missing its sealed complete evidence scope.';
3607
+ const evidence = item.kind === 'observe'
3608
+ ? currentLevelTwoScope(ctx, item)
3609
+ : item.kind === 'reflect'
3610
+ ? currentLevelThreeScope(ctx, item)
3611
+ : null;
3612
+ if (!evidence || observationScopeContextHash(evidence) !== expected) {
3613
+ return 'the complete current evidence scope changed after its assessment was sealed.';
3614
+ }
3615
+ return null;
3616
+ }
3617
+ function currentLevelTwoScope(ctx, item) {
3618
+ const subject = item.evidence[0]?.observationSubject;
3619
+ if (!subject)
3620
+ return null;
3621
+ const selected = new Set(item.evidence.map((entry) => entry.observationFactId).filter(Boolean));
3622
+ const rows = ctx.store.db
3623
+ .prepare(`SELECT f.id, f.claim, f.item_id, p.id AS page_id, p.slug,
3624
+ g.eligibility, g.traversable
3625
+ FROM facts f JOIN pages p ON p.id = f.page_id
3626
+ JOIN graph_fact_status g ON g.fact_id = f.id
3627
+ WHERE f.valid_to IS NULL
3628
+ AND f.confidence >= 0.5
3629
+ AND p.role = 'knowledge'
3630
+ AND p.derived_hash = p.body_hash
3631
+ AND g.subject_entity = ?
3632
+ ORDER BY p.slug, f.id`)
3633
+ .all(subject);
3634
+ const evidence = [];
3635
+ let characters = 0;
3636
+ for (const row of rows) {
3637
+ const nextCharacters = row.id.length + row.claim.length + row.slug.length;
3638
+ if (evidence.length >= 80 || characters + nextCharacters > 80_000)
3639
+ return null;
3640
+ const proofs = proofGroupsForFact(ctx.store, row.id, row.page_id, row.item_id);
3641
+ const conflictReason = row.eligibility.startsWith('conflict_') ? row.eligibility : null;
3642
+ const canSupport = !conflictReason && row.eligibility === 'eligible' && row.traversable === 1 && proofs.size > 0;
3643
+ const isSelected = selected.has(row.id);
3644
+ evidence.push({
3645
+ id: row.id,
3646
+ claim: row.claim,
3647
+ slug: row.slug,
3648
+ status: isSelected
3649
+ ? 'selected_support'
3650
+ : conflictReason
3651
+ ? 'counterevidence'
3652
+ : canSupport
3653
+ ? 'eligible_context'
3654
+ : 'ineligible_context',
3655
+ ...(!isSelected && !canSupport
3656
+ ? {
3657
+ statusReason: conflictReason ?? `graph_${row.eligibility}_${row.traversable ? 'linked' : 'held'}`,
3658
+ }
3659
+ : {}),
3660
+ });
3661
+ characters += nextCharacters;
3662
+ }
3663
+ return evidence.sort((a, b) => `${a.slug}\0${a.id}`.localeCompare(`${b.slug}\0${b.id}`));
3664
+ }
3665
+ function currentLevelThreeScope(ctx, item) {
3666
+ const rows = ctx.store.db
3667
+ .prepare(`SELECT oe.id, oe.source_slug AS slug, oe.payload
3668
+ FROM observation_entries oe JOIN pages p ON p.id = oe.source_page
3669
+ WHERE oe.eligible = 1 AND oe.disposition = 'active'
3670
+ ORDER BY p.updated_at DESC, oe.source_slug, oe.marker_line DESC, oe.id LIMIT 41`)
3671
+ .all();
3672
+ if (rows.length > 40)
3673
+ return null;
3674
+ const selected = new Set(item.evidence.map((entry) => entry.reflectionObservationId).filter(Boolean));
3675
+ const evidence = [];
3676
+ let leafCount = 0;
3677
+ let characters = 0;
3678
+ for (const row of rows) {
3679
+ const leaves = ctx.store.db
3680
+ .prepare(`SELECT f.id, f.claim, p.slug
3681
+ FROM observation_evidence oe JOIN facts f ON f.id = oe.fact_id
3682
+ JOIN pages p ON p.id = f.page_id
3683
+ WHERE oe.observation_id = ? AND f.valid_to IS NULL
3684
+ ORDER BY oe.ordinal`)
3685
+ .all(row.id);
3686
+ const expected = ctx.store.db
3687
+ .prepare('SELECT count(*) AS count FROM observation_evidence WHERE observation_id = ?')
3688
+ .get(row.id);
3689
+ const nextCharacters = row.id.length +
3690
+ row.slug.length +
3691
+ row.payload.length +
3692
+ leaves.reduce((sum, leaf) => sum + leaf.id.length + leaf.slug.length + leaf.claim.length, 0);
3693
+ if (leaves.length !== expected.count ||
3694
+ leafCount + leaves.length > 120 ||
3695
+ characters + nextCharacters > 80_000) {
3696
+ return null;
3697
+ }
3698
+ evidence.push({
3699
+ id: row.id,
3700
+ claim: row.payload,
3701
+ slug: row.slug,
3702
+ status: selected.has(row.id) ? 'selected_support' : 'eligible_context',
3703
+ leaves,
3704
+ });
3705
+ leafCount += leaves.length;
3706
+ characters += nextCharacters;
3707
+ }
3708
+ return evidence.sort((a, b) => `${a.slug}\0${a.id}`.localeCompare(`${b.slug}\0${b.id}`));
3709
+ }
3490
3710
  async function curationPageEvidenceIssue(ctx, item) {
3491
3711
  for (const entry of item.evidence.filter((candidate) => candidate.type === 'page')) {
3492
3712
  if (!entry.fingerprint) {
@@ -4201,15 +4421,11 @@ export function maintenancePlanStatusAfterApply(plan) {
4201
4421
  }
4202
4422
  else if (statuses.includes('proposed'))
4203
4423
  return 'ready';
4204
- else if (plan.items.some((item) => ['dependency_conflict', 'dependency_unmet', 'snapshot_drift'].includes(item.statusCode ?? ''))) {
4205
- // These items need a new plan, unlike budget and verification deferrals. Keeping this plan
4206
- // active would make the next cycle resume a terminal item forever instead of replanning it.
4207
- return 'failed';
4208
- }
4209
4424
  else if (statuses.every((value) => value === 'applied' || value === 'rejected'))
4210
4425
  return 'completed';
4211
- else if (statuses.includes('applied'))
4212
- return 'partially_completed';
4426
+ // Every status left here is terminal for this sealed item. That includes untyped `blocked` rows
4427
+ // persisted before status codes existed: keeping a mixed applied/blocked plan nonterminal makes
4428
+ // it look resumable even though apply has no legal next step.
4213
4429
  return 'failed';
4214
4430
  }
4215
4431
  function setPlanStatus(ctx, planId, status) {
@@ -4219,31 +4435,31 @@ function setPlanStatus(ctx, planId, status) {
4219
4435
  }
4220
4436
  export function finalizeRetryableMaintenancePlans(ctx) {
4221
4437
  requireWritable(ctx);
4222
- const result = ctx.store.db
4223
- .prepare(`UPDATE maintenance_plans AS plan
4224
- SET status = 'failed', updated_at = ?
4225
- WHERE plan.status = 'partially_completed'
4226
- AND EXISTS (
4227
- SELECT 1 FROM maintenance_items item
4228
- WHERE item.plan_id = plan.id
4229
- AND item.status_code IN ('dependency_conflict', 'dependency_unmet', 'snapshot_drift')
4230
- )
4231
- AND NOT EXISTS (
4232
- SELECT 1 FROM maintenance_items item
4233
- WHERE item.plan_id = plan.id
4234
- AND item.status IN ('proposed', 'approved', 'applying', 'verification_pending')
4235
- )
4236
- AND NOT EXISTS (
4237
- SELECT 1 FROM maintenance_items item
4238
- WHERE item.plan_id = plan.id
4239
- AND item.status IN ('blocked', 'stale', 'verification_failed')
4240
- AND NOT (
4241
- (item.status = 'blocked' AND item.status_code IN ('dependency_conflict', 'dependency_unmet'))
4242
- OR (item.status = 'stale' AND item.status_code = 'snapshot_drift')
4243
- )
4244
- )`)
4245
- .run(new Date().toISOString());
4246
- return result.changes;
4438
+ const now = new Date().toISOString();
4439
+ return ctx.store.transaction(() => {
4440
+ const completed = ctx.store.db
4441
+ .prepare(`UPDATE maintenance_plans AS plan
4442
+ SET status = 'completed', updated_at = ?
4443
+ WHERE plan.status NOT IN ('completed', 'failed', 'superseded')
4444
+ AND EXISTS (SELECT 1 FROM maintenance_items item WHERE item.plan_id = plan.id)
4445
+ AND NOT EXISTS (
4446
+ SELECT 1 FROM maintenance_items item
4447
+ WHERE item.plan_id = plan.id
4448
+ AND item.status NOT IN ('applied', 'rejected')
4449
+ )`)
4450
+ .run(now).changes;
4451
+ const failed = ctx.store.db
4452
+ .prepare(`UPDATE maintenance_plans AS plan
4453
+ SET status = 'failed', updated_at = ?
4454
+ WHERE plan.status NOT IN ('completed', 'failed', 'superseded')
4455
+ AND NOT EXISTS (
4456
+ SELECT 1 FROM maintenance_items item
4457
+ WHERE item.plan_id = plan.id
4458
+ AND item.status IN ('proposed', 'approved', 'applying', 'verification_pending')
4459
+ )`)
4460
+ .run(now).changes;
4461
+ return completed + failed;
4462
+ });
4247
4463
  }
4248
4464
  /**
4249
4465
  * A dependency-deferred plan was sealed against the pre-apply snapshot. Once the same run starts