@tenphi/akno-core 0.6.0 → 0.7.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.
Files changed (79) hide show
  1. package/config/default.jsonc +7 -5
  2. package/dist/bench/answer.js +5 -2
  3. package/dist/bench/answer.js.map +1 -1
  4. package/dist/config/schema.d.ts +8 -0
  5. package/dist/config/schema.d.ts.map +1 -1
  6. package/dist/config/schema.js +2 -0
  7. package/dist/config/schema.js.map +1 -1
  8. package/dist/index/derive.d.ts.map +1 -1
  9. package/dist/index/derive.js +17 -0
  10. package/dist/index/derive.js.map +1 -1
  11. package/dist/index/graph.d.ts +1 -0
  12. package/dist/index/graph.d.ts.map +1 -1
  13. package/dist/index/graph.js +50 -0
  14. package/dist/index/graph.js.map +1 -1
  15. package/dist/index/indexer.d.ts +3 -0
  16. package/dist/index/indexer.d.ts.map +1 -1
  17. package/dist/index/indexer.js +34 -3
  18. package/dist/index/indexer.js.map +1 -1
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/kb/page.d.ts +4 -1
  23. package/dist/kb/page.d.ts.map +1 -1
  24. package/dist/kb/page.js +5 -0
  25. package/dist/kb/page.js.map +1 -1
  26. package/dist/maintenance/dream.d.ts +1 -1
  27. package/dist/maintenance/dream.d.ts.map +1 -1
  28. package/dist/maintenance/dream.js +547 -198
  29. package/dist/maintenance/dream.js.map +1 -1
  30. package/dist/maintenance/observation-migration.d.ts +17 -0
  31. package/dist/maintenance/observation-migration.d.ts.map +1 -0
  32. package/dist/maintenance/observation-migration.js +217 -0
  33. package/dist/maintenance/observation-migration.js.map +1 -0
  34. package/dist/maintenance/observe.d.ts +20 -3
  35. package/dist/maintenance/observe.d.ts.map +1 -1
  36. package/dist/maintenance/observe.js +95 -24
  37. package/dist/maintenance/observe.js.map +1 -1
  38. package/dist/maintenance/path-policy.d.ts.map +1 -1
  39. package/dist/maintenance/path-policy.js +2 -2
  40. package/dist/maintenance/path-policy.js.map +1 -1
  41. package/dist/maintenance/plans.d.ts +28 -1
  42. package/dist/maintenance/plans.d.ts.map +1 -1
  43. package/dist/maintenance/plans.js +360 -17
  44. package/dist/maintenance/plans.js.map +1 -1
  45. package/dist/observations/marker.d.ts +27 -0
  46. package/dist/observations/marker.d.ts.map +1 -0
  47. package/dist/observations/marker.js +238 -0
  48. package/dist/observations/marker.js.map +1 -0
  49. package/dist/observations/projection.d.ts +23 -0
  50. package/dist/observations/projection.d.ts.map +1 -0
  51. package/dist/observations/projection.js +349 -0
  52. package/dist/observations/projection.js.map +1 -0
  53. package/dist/open.d.ts +3 -0
  54. package/dist/open.d.ts.map +1 -1
  55. package/dist/open.js +2 -0
  56. package/dist/open.js.map +1 -1
  57. package/dist/ops/answer.d.ts.map +1 -1
  58. package/dist/ops/answer.js +74 -12
  59. package/dist/ops/answer.js.map +1 -1
  60. package/dist/ops/context.js +3 -1
  61. package/dist/ops/context.js.map +1 -1
  62. package/dist/ops/graph.d.ts.map +1 -1
  63. package/dist/ops/graph.js +22 -2
  64. package/dist/ops/graph.js.map +1 -1
  65. package/dist/ops/read.d.ts.map +1 -1
  66. package/dist/ops/read.js +9 -3
  67. package/dist/ops/read.js.map +1 -1
  68. package/dist/recall/assemble.d.ts +1 -0
  69. package/dist/recall/assemble.d.ts.map +1 -1
  70. package/dist/recall/assemble.js +18 -7
  71. package/dist/recall/assemble.js.map +1 -1
  72. package/dist/store/db.d.ts.map +1 -1
  73. package/dist/store/db.js +4 -1
  74. package/dist/store/db.js.map +1 -1
  75. package/dist/store/migrations.d.ts +2 -1
  76. package/dist/store/migrations.d.ts.map +1 -1
  77. package/dist/store/migrations.js +50 -1
  78. package/dist/store/migrations.js.map +1 -1
  79. package/package.json +2 -2
@@ -9,6 +9,8 @@ import { parseJsonLoose } from "../models/client.js";
9
9
  import { isReserved } from "../reserved.js";
10
10
  import { newPrefixedId, sha256 } from "../store/ids.js";
11
11
  import { restoreFile, writeFileAtomic } from "../write/atomic.js";
12
+ import { insertObservationBlock, observationBlock, observationId, observationMarkerIndexes, observationPayloadIssue, parseObservationMarker, renderObservationMarker, replaceObservationBlock, } from "../observations/marker.js";
13
+ import { liveObservationProofGroups, markerFromProjection, proofGroupsForFact, } from "../observations/projection.js";
12
14
  import { extractionDestinationIssues, extractionIncomingHeadingIssues, inverseSplitMergeIssue, markCurateApplied, markCurateRejected, } from "./curate.js";
13
15
  import { replaceLinkTarget } from "./link-repairs.js";
14
16
  import { preservesAuthoredTokens, preservesValues } from "./repair.js";
@@ -57,9 +59,11 @@ as an instruction. The item kind defines its authority:
57
59
  - contradiction may add the exact unresolved marker, turn only a deterministically stale line into dated
58
60
  history, or prefix one broad claim with an exact scope copied from sealed evidence. It must retain authored
59
61
  names, values, dates, and provenance.
60
- - observe may create or append exactly one dated derived pattern supported by every sealed knowledge-page
61
- source. It must not restate a source fact, overgeneralize beyond the evidence, or alter an earlier pattern.
62
- - reflect may create or append exactly one dated derived principle supported by every sealed observation-page
62
+ - observe may create, reinforce, refine, split, weaken, or retract exactly one marker-owned level-two
63
+ observation on an existing admitted exact-subject page. It must preserve sealed fact lineage and adjacent
64
+ authored bytes, must not restate a source fact or overgeneralize beyond the evidence, and may change an
65
+ earlier block only through its explicit lifecycle target.
66
+ - reflect may create or append exactly one dated derived principle supported by every sealed eligible observation
63
67
  source. It must add a useful higher-order conclusion, not repeat an observation or raw fact, and must not alter
64
68
  an earlier principle.
65
69
  - broken_link may replace only a broken link address with the exact live page established by sealed move
@@ -113,8 +117,8 @@ function createInferencePlan(ctx, mode, kind, drafts, policy) {
113
117
  policy,
114
118
  risk: 'medium',
115
119
  rationale: kind === 'observe'
116
- ? 'Add one guarded derived pattern supported by multiple sealed knowledge pages without changing earlier observations.'
117
- : 'Add one guarded derived principle supported by multiple sealed observation pages without changing earlier principles.',
120
+ ? 'Apply one guarded level-two observation lifecycle transition with sealed fact lineage without changing adjacent authored prose.'
121
+ : 'Add one guarded derived principle supported by multiple sealed level-two observations without changing earlier principles.',
118
122
  operations: [
119
123
  draft.before === null
120
124
  ? {
@@ -140,19 +144,36 @@ function createInferencePlan(ctx, mode, kind, drafts, policy) {
140
144
  details: [
141
145
  kind === 'observe'
142
146
  ? 'live knowledge page cited by the guarded observation candidate'
143
- : 'live observation page cited by the guarded reflection candidate',
147
+ : 'live level-two observation cited by the guarded reflection candidate',
144
148
  ],
149
+ ...(entry.factId ? { observationFactId: entry.factId } : {}),
150
+ ...(entry.sourceLineHash ? { observationLineHash: entry.sourceLineHash } : {}),
151
+ ...(entry.proofGroups ? { observationProofGroups: entry.proofGroups } : {}),
152
+ ...(draft.observationId ? { observationId: draft.observationId } : {}),
153
+ ...(draft.observationSubject ? { observationSubject: draft.observationSubject } : {}),
154
+ ...(draft.observationDisposition ? { observationDisposition: draft.observationDisposition } : {}),
155
+ ...(draft.observationProofCount !== undefined
156
+ ? { observationProofCount: draft.observationProofCount }
157
+ : {}),
158
+ ...(draft.observationOutcome ? { observationOutcome: draft.observationOutcome } : {}),
159
+ ...(draft.observationTargetId ? { observationTargetId: draft.observationTargetId } : {}),
160
+ ...(draft.observationPayloadHash ? { observationPayloadHash: draft.observationPayloadHash } : {}),
161
+ ...(draft.observationQualificationIssue
162
+ ? { observationQualificationIssue: draft.observationQualificationIssue }
163
+ : {}),
164
+ ...(entry.reflectionObservationId ? { reflectionObservationId: entry.reflectionObservationId } : {}),
165
+ ...(entry.reflectionPayloadHash ? { reflectionPayloadHash: entry.reflectionPayloadHash } : {}),
145
166
  })),
146
167
  checks: [
147
168
  { name: `${kind} mission guardrails`, status: 'passed' },
148
169
  {
149
170
  name: kind === 'observe'
150
- ? 'at least two distinct live knowledge sources'
171
+ ? 'at least two independent sealed level-one proof groups'
151
172
  : 'at least three distinct live observation sources',
152
173
  status: 'passed',
153
174
  },
154
175
  {
155
- name: `append-only derived ${kind === 'observe' ? 'observation' : 'principle'} shape`,
176
+ name: kind === 'observe' ? 'marker-owned observation block shape' : 'append-only derived principle shape',
156
177
  status: 'passed',
157
178
  },
158
179
  ],
@@ -2277,7 +2298,7 @@ async function verifyApplied(ctx, item, operations) {
2277
2298
  }
2278
2299
  const parsed = parsePage(operation.relPath, content);
2279
2300
  const row = ctx.store.db
2280
- .prepare('SELECT id, slug, rel_path, body_hash, role, type, remember_management, dream_management FROM pages WHERE rel_path = ?')
2301
+ .prepare('SELECT id, slug, rel_path, body_hash, role, type, remember_management, observe_management, dream_management FROM pages WHERE rel_path = ?')
2281
2302
  .get(operation.relPath);
2282
2303
  if (!row)
2283
2304
  return `${operation.relPath} is missing from the structural index.`;
@@ -2294,10 +2315,13 @@ async function verifyApplied(ctx, item, operations) {
2294
2315
  canonical = parsed;
2295
2316
  canonicalPageId = row.id;
2296
2317
  }
2297
- const expectedRole = isInferenceKind(item.kind) ? 'inference' : 'knowledge';
2318
+ const expectedRole = item.kind === 'reflect' ? 'inference' : 'knowledge';
2298
2319
  if (row.role !== expectedRole) {
2299
2320
  return `${operation.relPath} is no longer live ${expectedRole}.`;
2300
2321
  }
2322
+ if (item.kind === 'observe' && row.observe_management !== 'integrate') {
2323
+ return `${operation.relPath} no longer permits observation integration.`;
2324
+ }
2301
2325
  if (item.kind === 'rule_drift') {
2302
2326
  const expected = item.evidence.find((entry) => entry.type === 'rule')?.expectedType;
2303
2327
  if (!depthRuleDrift && (!expected || row.type !== expected)) {
@@ -2370,7 +2394,7 @@ async function verifyApplied(ctx, item, operations) {
2370
2394
  const shapeIssue = observationOperationIssue(ctx, item, operations);
2371
2395
  if (shapeIssue)
2372
2396
  return shapeIssue;
2373
- const evidenceIssue = await observationEvidenceIssue(ctx, item);
2397
+ const evidenceIssue = await observationEvidenceIssue(ctx, item, 'after');
2374
2398
  if (evidenceIssue)
2375
2399
  return evidenceIssue;
2376
2400
  }
@@ -3030,6 +3054,8 @@ function depthRuleDriftOperationIssue(ctx, item, operations, entry, state) {
3030
3054
  return null;
3031
3055
  }
3032
3056
  function observationOperationIssue(ctx, item, operations) {
3057
+ if (item.kind === 'observe')
3058
+ return coLocatedObservationOperationIssue(ctx, item, operations);
3033
3059
  const operation = operations[0];
3034
3060
  if (!operation ||
3035
3061
  (operation.type !== 'create' && operation.type !== 'replace') ||
@@ -3048,7 +3074,16 @@ function observationOperationIssue(ctx, item, operations) {
3048
3074
  const minEvidence = item.kind === 'reflect'
3049
3075
  ? Math.max(3, ctx.config.maintenance.observe.minEvidence)
3050
3076
  : ctx.config.maintenance.observe.minEvidence;
3051
- if (sources.length < minEvidence ||
3077
+ const indexedReflection = item.kind === 'reflect' && sources.every((entry) => entry.reflectionObservationId);
3078
+ if (indexedReflection) {
3079
+ if (sources.length < minEvidence ||
3080
+ sources.length !== item.evidence.length ||
3081
+ new Set(sources.map((entry) => entry.reflectionObservationId)).size !== sources.length ||
3082
+ sources.some((entry) => !entry.fingerprint || !entry.reflectionPayloadHash)) {
3083
+ return 'a reflection requires distinct eligible level-two observation evidence';
3084
+ }
3085
+ }
3086
+ else if (sources.length < minEvidence ||
3052
3087
  sources.length !== item.evidence.length ||
3053
3088
  new Set(sources.map((entry) => entry.source)).size !== sources.length ||
3054
3089
  sources.some((entry) => !entry.fingerprint)) {
@@ -3063,7 +3098,7 @@ function observationOperationIssue(ctx, item, operations) {
3063
3098
  }
3064
3099
  const afterData = after.frontmatter.data;
3065
3100
  const evidence = stringArray(afterData.evidence);
3066
- const sourceSlugs = sources.map((entry) => entry.source);
3101
+ const sourceSlugs = [...new Set(sources.map((entry) => entry.source))];
3067
3102
  if (afterData.derived !== true) {
3068
3103
  return 'the inference page must remain explicitly derived';
3069
3104
  }
@@ -3112,7 +3147,306 @@ function observationOperationIssue(ctx, item, operations) {
3112
3147
  }
3113
3148
  return null;
3114
3149
  }
3115
- async function observationEvidenceIssue(ctx, item) {
3150
+ function coLocatedObservationOperationIssue(ctx, item, operations) {
3151
+ const operation = operations[0];
3152
+ if (!operation || operation.type !== 'replace' || operations.length !== 1) {
3153
+ return 'an observation must change exactly one existing admitted page';
3154
+ }
3155
+ const target = ctx.store.db
3156
+ .prepare('SELECT id, slug, rel_path, role, observe_management, about FROM pages WHERE slug = ?')
3157
+ .get(item.subject);
3158
+ if (!target ||
3159
+ target.rel_path !== operation.relPath ||
3160
+ target.role !== 'knowledge' ||
3161
+ target.observe_management !== 'integrate') {
3162
+ return 'the observation target is not an admitted knowledge page';
3163
+ }
3164
+ const sources = item.evidence.filter((entry) => entry.type === 'page');
3165
+ const outcome = sources[0]?.observationOutcome ?? 'create';
3166
+ const closesInvalidLineage = outcome === 'weaken' || outcome === 'retract';
3167
+ if (sources.length < 2 ||
3168
+ sources.length !== item.evidence.length ||
3169
+ sources.some((entry) => (!closesInvalidLineage && !entry.fingerprint) ||
3170
+ !entry.observationFactId ||
3171
+ !entry.observationLineHash ||
3172
+ !entry.observationProofGroups?.length ||
3173
+ !entry.observationId ||
3174
+ !entry.observationSubject ||
3175
+ (closesInvalidLineage
3176
+ ? entry.observationDisposition !== (outcome === 'weaken' ? 'weakened' : 'retracted')
3177
+ : entry.observationDisposition !== 'active') ||
3178
+ entry.observationProofCount === undefined)) {
3179
+ return 'an observation requires exact sealed fact lineage';
3180
+ }
3181
+ const first = sources[0];
3182
+ if (sources.some((entry) => entry.observationId !== first.observationId ||
3183
+ entry.observationSubject !== first.observationSubject ||
3184
+ entry.observationProofCount !== first.observationProofCount)) {
3185
+ return 'observation evidence disagrees about marker identity';
3186
+ }
3187
+ const factIds = sources.map((entry) => entry.observationFactId);
3188
+ if (new Set(factIds).size !== factIds.length)
3189
+ return 'observation evidence contains a duplicate fact';
3190
+ const proofGroups = new Set(sources.flatMap((entry) => entry.observationProofGroups));
3191
+ if (proofGroups.size < Math.max(2, ctx.config.maintenance.observe.minEvidence)) {
3192
+ return 'an observation needs the configured number of independent proof groups';
3193
+ }
3194
+ if (proofGroups.size !== first.observationProofCount)
3195
+ return 'observation proof count is not recomputable';
3196
+ const afterPage = parsePage(operation.relPath, operation.after);
3197
+ const targetId = first.observationTargetId ?? first.observationId;
3198
+ const markerIndex = observationMarkerIndexes(afterPage.lines, targetId)[0] ?? -1;
3199
+ if (markerIndex < 0)
3200
+ return 'the observation marker is missing from the proposed page';
3201
+ const marker = parseObservationMarker(afterPage.lines[markerIndex]);
3202
+ const payload = afterPage.lines[markerIndex + 1]?.trim() ?? '';
3203
+ if (!marker || marker.subject !== first.observationSubject) {
3204
+ return 'the proposed observation marker does not match its sealed lineage';
3205
+ }
3206
+ if (closesInvalidLineage) {
3207
+ const disposition = outcome === 'weaken' ? 'weakened' : 'retracted';
3208
+ if (marker.disposition !== disposition ||
3209
+ marker.proofCount !== first.observationProofCount ||
3210
+ sources.some((entry) => entry.observationPayloadHash !== first.observationPayloadHash ||
3211
+ entry.observationQualificationIssue !== first.observationQualificationIssue)) {
3212
+ return `a ${outcome} proposal does not preserve its sealed invalid lineage`;
3213
+ }
3214
+ const expected = replaceObservationBlock(operation.before, targetId, `${renderObservationMarker(marker)}\n${payload}`);
3215
+ return expected === operation.after
3216
+ ? null
3217
+ : `the observation ${outcome} changed bytes outside its owned block`;
3218
+ }
3219
+ if (outcome === 'split') {
3220
+ if (marker.disposition !== 'superseded')
3221
+ return 'a split must preserve its prior marker as superseded';
3222
+ const beforePage = parsePage(operation.relPath, operation.before);
3223
+ const beforeIndex = observationMarkerIndexes(beforePage.lines, targetId)[0] ?? -1;
3224
+ const beforeMarker = beforeIndex >= 0 ? parseObservationMarker(beforePage.lines[beforeIndex]) : null;
3225
+ const beforePayload = beforeIndex >= 0 ? (beforePage.lines[beforeIndex + 1]?.trim() ?? '') : '';
3226
+ if (!beforeMarker ||
3227
+ beforeMarker.disposition !== 'active' ||
3228
+ renderObservationMarker({ ...beforeMarker, disposition: 'superseded' }) !==
3229
+ renderObservationMarker(marker) ||
3230
+ payload !== beforePayload) {
3231
+ return 'a split must preserve the exact prior block as superseded';
3232
+ }
3233
+ const replacementLines = afterPage.lines.slice(markerIndex, markerIndex + 8);
3234
+ const activeMarkers = replacementLines
3235
+ .map(parseObservationMarker)
3236
+ .filter((entry) => entry !== null && entry.disposition === 'active');
3237
+ if (activeMarkers.length !== 2 ||
3238
+ activeMarkers.some((entry) => entry.subject !== first.observationSubject || entry.proofCount !== first.observationProofCount)) {
3239
+ return 'a split must produce exactly two active observations with the sealed lineage';
3240
+ }
3241
+ const activePayloads = [replacementLines[4]?.trim() ?? '', replacementLines[7]?.trim() ?? ''];
3242
+ const evidenceSlugs = sources.map((entry) => entry.source);
3243
+ const expectedEvidence = sources.map((entry) => ({
3244
+ factId: entry.observationFactId,
3245
+ sourceLineHash: entry.observationLineHash,
3246
+ proofGroups: entry.observationProofGroups,
3247
+ }));
3248
+ const patterns = activePayloads.map((entry) => /^- \*\*Observation:\*\* (.+?) Evidence: /.exec(entry)?.[1]?.trim() ?? '');
3249
+ const activeIds = activeMarkers.map((entry) => entry.id);
3250
+ if (patterns.some((entry) => !entry) ||
3251
+ activePayloads.some((entry) => observationPayloadIssue(entry, evidenceSlugs) !== null) ||
3252
+ activeMarkers.some((entry) => renderObservationMarker(entry) !==
3253
+ renderObservationMarker({
3254
+ id: entry.id,
3255
+ subject: first.observationSubject,
3256
+ disposition: 'active',
3257
+ evidence: expectedEvidence,
3258
+ proofCount: first.observationProofCount,
3259
+ })) ||
3260
+ new Set(activeIds).size !== 2 ||
3261
+ activeIds.includes(targetId) ||
3262
+ activeIds.some((id, index) => id !== observationId(first.observationSubject, patterns[index]))) {
3263
+ return 'a split must produce two distinct deterministic blocks with exact sealed lineage';
3264
+ }
3265
+ const newline = operation.after.includes('\r\n') ? '\r\n' : '\n';
3266
+ const replacement = [
3267
+ `${renderObservationMarker(marker)}${newline}${payload}`,
3268
+ observationBlock(activeMarkers[0], patterns[0], evidenceSlugs),
3269
+ observationBlock(activeMarkers[1], patterns[1], evidenceSlugs),
3270
+ ].join(`${newline}${newline}`);
3271
+ const expected = replaceObservationBlock(operation.before, targetId, replacement);
3272
+ return expected === operation.after
3273
+ ? null
3274
+ : 'the observation split changed bytes outside its owned block';
3275
+ }
3276
+ if (marker.proofCount !== first.observationProofCount || marker.disposition !== 'active') {
3277
+ return 'the proposed observation marker does not preserve active proof lineage';
3278
+ }
3279
+ const pattern = /^- \*\*Observation:\*\* (.+?) Evidence: /.exec(payload)?.[1]?.trim();
3280
+ if (!pattern)
3281
+ return 'the proposed observation lacks visible derived wording';
3282
+ const expectedMarker = {
3283
+ ...marker,
3284
+ evidence: sources.map((entry) => ({
3285
+ factId: entry.observationFactId,
3286
+ sourceLineHash: entry.observationLineHash,
3287
+ proofGroups: entry.observationProofGroups,
3288
+ })),
3289
+ };
3290
+ const expectedBlock = observationBlock(expectedMarker, pattern, sources.map((entry) => entry.source));
3291
+ const expected = outcome === 'create'
3292
+ ? insertObservationBlock(operation.before, expectedBlock)
3293
+ : replaceObservationBlock(operation.before, targetId, expectedBlock);
3294
+ return expected === operation.after
3295
+ ? null
3296
+ : 'the observation proposal changed bytes outside its owned block or section delimiter';
3297
+ }
3298
+ async function observationEvidenceIssue(ctx, item, phase = 'before') {
3299
+ if (item.kind === 'observe') {
3300
+ const first = item.evidence[0];
3301
+ const lifecycle = first?.observationOutcome;
3302
+ if ((lifecycle === 'weaken' || lifecycle === 'retract') && first) {
3303
+ const expectedDisposition = lifecycle === 'weaken' ? 'weakened' : 'retracted';
3304
+ const observation = ctx.store.db
3305
+ .prepare(`SELECT id, source_slug, subject_entity, disposition, payload_hash, proof_count, eligible, issue
3306
+ FROM observation_entries WHERE id = ?`)
3307
+ .get(first.observationId);
3308
+ if (!observation ||
3309
+ observation.source_slug !== item.subject ||
3310
+ observation.subject_entity !== first.observationSubject ||
3311
+ observation.payload_hash !== first.observationPayloadHash ||
3312
+ observation.proof_count !== first.observationProofCount) {
3313
+ return 'the observation no longer matches the sealed lifecycle target.';
3314
+ }
3315
+ // Verification runs after re-indexing, when the requested disposition is already current.
3316
+ if (observation.disposition === expectedDisposition)
3317
+ return null;
3318
+ if (observation.disposition !== 'active' ||
3319
+ observation.eligible !== 0 ||
3320
+ observation.issue !== first.observationQualificationIssue) {
3321
+ return 'the observation qualification changed after its lifecycle transition was sealed.';
3322
+ }
3323
+ const currentEvidence = ctx.store.db
3324
+ .prepare(`SELECT fact_id, source_line_hash, proof_groups
3325
+ FROM observation_evidence WHERE observation_id = ? ORDER BY ordinal`)
3326
+ .all(first.observationId);
3327
+ if (currentEvidence.length !== item.evidence.length ||
3328
+ currentEvidence.some((entry, index) => {
3329
+ const sealed = item.evidence[index];
3330
+ return (entry.fact_id !== sealed.observationFactId ||
3331
+ entry.source_line_hash !== sealed.observationLineHash ||
3332
+ !sameStringSet(JSON.parse(entry.proof_groups), sealed.observationProofGroups ?? []));
3333
+ })) {
3334
+ return 'the invalid observation lineage changed after it was sealed.';
3335
+ }
3336
+ const currentMarker = markerFromProjection(ctx.store, first.observationId);
3337
+ if (!currentMarker)
3338
+ return 'the observation marker is no longer projected.';
3339
+ const survivingProofs = liveObservationProofGroups(ctx.store, currentMarker);
3340
+ if ((lifecycle === 'weaken' && survivingProofs.size === 0) ||
3341
+ (lifecycle === 'retract' && survivingProofs.size !== 0)) {
3342
+ return `the observation no longer qualifies for ${lifecycle}.`;
3343
+ }
3344
+ for (const entry of item.evidence) {
3345
+ if (!entry.fingerprint || entry.source.startsWith('fact:'))
3346
+ continue;
3347
+ const page = ctx.store.db.prepare('SELECT rel_path FROM pages WHERE slug = ?').get(entry.source);
3348
+ const bytes = page
3349
+ ? await fsp.readFile(path.join(ctx.config.aknoPath, page.rel_path), 'utf8').catch(() => null)
3350
+ : null;
3351
+ if (bytes === null || sha256(bytes) !== entry.fingerprint) {
3352
+ return `${entry.source} changed after the observation lifecycle transition was sealed.`;
3353
+ }
3354
+ }
3355
+ return null;
3356
+ }
3357
+ for (const entry of item.evidence) {
3358
+ const factId = entry.observationFactId;
3359
+ if (!factId || !entry.observationLineHash || !entry.observationProofGroups) {
3360
+ return 'observation evidence is missing its exact fact locator.';
3361
+ }
3362
+ const fact = ctx.store.db
3363
+ .prepare(`SELECT f.source_line_hash, f.valid_to, f.item_id, p.id AS page_id, p.slug, p.rel_path,
3364
+ p.role, p.body_hash, p.derived_hash, g.subject_entity, g.eligibility, g.traversable
3365
+ FROM facts f JOIN pages p ON p.id = f.page_id
3366
+ LEFT JOIN graph_fact_status g ON g.fact_id = f.id WHERE f.id = ?`)
3367
+ .get(factId);
3368
+ if (!fact ||
3369
+ fact.slug !== entry.source ||
3370
+ fact.source_line_hash !== entry.observationLineHash ||
3371
+ fact.valid_to !== null ||
3372
+ fact.role !== 'knowledge' ||
3373
+ fact.body_hash !== fact.derived_hash ||
3374
+ fact.subject_entity !== entry.observationSubject ||
3375
+ fact.eligibility !== 'eligible' ||
3376
+ fact.traversable !== 1) {
3377
+ return `${entry.source} no longer contains the sealed eligible fact.`;
3378
+ }
3379
+ const proofs = proofGroupsForFact(ctx.store, factId, fact.page_id, fact.item_id);
3380
+ if (proofs.size !== entry.observationProofGroups.length ||
3381
+ entry.observationProofGroups.some((proof) => !proofs.has(proof))) {
3382
+ return `${entry.source} no longer has the sealed independent support.`;
3383
+ }
3384
+ const bytes = await fsp
3385
+ .readFile(path.join(ctx.config.aknoPath, fact.rel_path), 'utf8')
3386
+ .catch(() => null);
3387
+ if (bytes === null || sha256(bytes) !== entry.fingerprint) {
3388
+ return `${entry.source} no longer matches its sealed observation evidence.`;
3389
+ }
3390
+ }
3391
+ if (phase === 'after') {
3392
+ const operation = item.operations[0];
3393
+ if (!operation || operation.type !== 'replace')
3394
+ return 'the applied observation operation is missing.';
3395
+ const appliedLifecycle = first?.observationOutcome ?? 'create';
3396
+ const page = parsePage(operation.relPath, operation.after);
3397
+ let expectedIds = [];
3398
+ if (appliedLifecycle === 'split' && first?.observationTargetId) {
3399
+ const targetIndex = observationMarkerIndexes(page.lines, first.observationTargetId)[0] ?? -1;
3400
+ expectedIds = page.lines
3401
+ .slice(targetIndex + 1, targetIndex + 8)
3402
+ .map(parseObservationMarker)
3403
+ .filter((entry) => entry !== null && entry.disposition === 'active')
3404
+ .map((entry) => entry.id);
3405
+ }
3406
+ else if (first?.observationId) {
3407
+ expectedIds = [first.observationId];
3408
+ }
3409
+ if (expectedIds.length !== (appliedLifecycle === 'split' ? 2 : 1)) {
3410
+ return 'the applied observation did not retain its expected active marker identity.';
3411
+ }
3412
+ for (const id of expectedIds) {
3413
+ const projected = ctx.store.db
3414
+ .prepare(`SELECT source_slug, subject_entity, disposition, eligible
3415
+ FROM observation_entries WHERE id = ?`)
3416
+ .get(id);
3417
+ if (!projected ||
3418
+ projected.source_slug !== item.subject ||
3419
+ projected.subject_entity !== first?.observationSubject ||
3420
+ projected.disposition !== 'active' ||
3421
+ projected.eligible !== 1) {
3422
+ return `the applied observation ${id} is not eligible in the rebuilt projection.`;
3423
+ }
3424
+ }
3425
+ }
3426
+ return null;
3427
+ }
3428
+ if (item.kind === 'reflect' && item.evidence.every((entry) => entry.reflectionObservationId)) {
3429
+ for (const entry of item.evidence) {
3430
+ const observation = ctx.store.db
3431
+ .prepare(`SELECT oe.source_slug, oe.payload_hash, oe.eligible, oe.disposition, p.rel_path
3432
+ FROM observation_entries oe JOIN pages p ON p.id = oe.source_page WHERE oe.id = ?`)
3433
+ .get(entry.reflectionObservationId);
3434
+ if (!observation ||
3435
+ observation.source_slug !== entry.source ||
3436
+ observation.payload_hash !== entry.reflectionPayloadHash ||
3437
+ observation.eligible !== 1 ||
3438
+ observation.disposition !== 'active') {
3439
+ return `${entry.source} is no longer eligible level-two observation evidence.`;
3440
+ }
3441
+ const bytes = await fsp
3442
+ .readFile(path.join(ctx.config.aknoPath, observation.rel_path), 'utf8')
3443
+ .catch(() => null);
3444
+ if (bytes === null || sha256(bytes) !== entry.fingerprint) {
3445
+ return `${entry.source} no longer matches its sealed reflection evidence.`;
3446
+ }
3447
+ }
3448
+ return null;
3449
+ }
3116
3450
  const expectedRole = item.kind === 'reflect' ? 'inference' : 'knowledge';
3117
3451
  const observationRoot = ctx.config.paths.observations.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
3118
3452
  for (const entry of item.evidence) {
@@ -3123,7 +3457,7 @@ async function observationEvidenceIssue(ctx, item) {
3123
3457
  if (item.kind === 'reflect' &&
3124
3458
  (entry.source === item.subject ||
3125
3459
  (entry.source !== observationRoot && !entry.source.startsWith(`${observationRoot}/`)))) {
3126
- return `${entry.source} is not an independent observation-page source.`;
3460
+ return `${entry.source} is not an independent legacy inference-page source.`;
3127
3461
  }
3128
3462
  let sourcePath;
3129
3463
  try {
@@ -3324,7 +3658,7 @@ async function preflightItem(ctx, item) {
3324
3658
  const issue = observationOperationIssue(ctx, item, operations);
3325
3659
  if (issue)
3326
3660
  return { status: 'blocked', detail: issue };
3327
- const evidenceIssue = await observationEvidenceIssue(ctx, item);
3661
+ const evidenceIssue = await observationEvidenceIssue(ctx, item, 'before');
3328
3662
  if (evidenceIssue)
3329
3663
  return { status: 'stale', detail: evidenceIssue };
3330
3664
  }
@@ -3434,7 +3768,7 @@ async function preflightItem(ctx, item) {
3434
3768
  canonical = parsed;
3435
3769
  if (operation.type === 'delete')
3436
3770
  mergeSource = parsed;
3437
- const allowedDeclaredRole = isInferenceKind(item.kind) ? 'inference' : 'knowledge';
3771
+ const allowedDeclaredRole = item.kind === 'reflect' ? 'inference' : 'knowledge';
3438
3772
  if (parsed.declaredRole && parsed.declaredRole !== allowedDeclaredRole) {
3439
3773
  return { status: 'blocked', detail: `${operation.relPath} is not declared as ${allowedDeclaredRole}` };
3440
3774
  }
@@ -3447,6 +3781,15 @@ async function preflightItem(ctx, item) {
3447
3781
  };
3448
3782
  }
3449
3783
  }
3784
+ if (item.kind === 'observe') {
3785
+ const currentPolicy = resolvePagePolicy(parsed, effectiveRule(parsed.slug, ctx.config.rules), ctx.config.paths.observations);
3786
+ if (currentPolicy.role !== 'knowledge' || currentPolicy.observe !== 'integrate') {
3787
+ return {
3788
+ status: 'blocked',
3789
+ detail: `${operation.relPath} no longer allows observation integration`,
3790
+ };
3791
+ }
3792
+ }
3450
3793
  if (item.kind === 'broken_link' &&
3451
3794
  index === 0 &&
3452
3795
  parsed.declaredManagement.dream !== 'hygiene' &&