@tenphi/akno-core 0.6.0 → 0.8.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/README.md +4 -1
- package/config/default.jsonc +7 -5
- package/dist/bench/answer.js +5 -2
- package/dist/bench/answer.js.map +1 -1
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +2 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/index/chunk.d.ts.map +1 -1
- package/dist/index/chunk.js +54 -17
- package/dist/index/chunk.js.map +1 -1
- package/dist/index/derive.d.ts.map +1 -1
- package/dist/index/derive.js +17 -0
- package/dist/index/derive.js.map +1 -1
- package/dist/index/graph.d.ts +3 -0
- package/dist/index/graph.d.ts.map +1 -1
- package/dist/index/graph.js +131 -3
- package/dist/index/graph.js.map +1 -1
- package/dist/index/indexer.d.ts +9 -0
- package/dist/index/indexer.d.ts.map +1 -1
- package/dist/index/indexer.js +66 -3
- package/dist/index/indexer.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kb/managed-lines.d.ts +3 -0
- package/dist/kb/managed-lines.d.ts.map +1 -1
- package/dist/kb/managed-lines.js +21 -1
- package/dist/kb/managed-lines.js.map +1 -1
- package/dist/kb/page.d.ts +4 -1
- package/dist/kb/page.d.ts.map +1 -1
- package/dist/kb/page.js +5 -0
- package/dist/kb/page.js.map +1 -1
- package/dist/maintenance/dream.d.ts +1 -1
- package/dist/maintenance/dream.d.ts.map +1 -1
- package/dist/maintenance/dream.js +547 -198
- package/dist/maintenance/dream.js.map +1 -1
- package/dist/maintenance/observation-migration.d.ts +17 -0
- package/dist/maintenance/observation-migration.d.ts.map +1 -0
- package/dist/maintenance/observation-migration.js +217 -0
- package/dist/maintenance/observation-migration.js.map +1 -0
- package/dist/maintenance/observe.d.ts +20 -3
- package/dist/maintenance/observe.d.ts.map +1 -1
- package/dist/maintenance/observe.js +95 -24
- package/dist/maintenance/observe.js.map +1 -1
- package/dist/maintenance/path-policy.d.ts.map +1 -1
- package/dist/maintenance/path-policy.js +2 -2
- package/dist/maintenance/path-policy.js.map +1 -1
- package/dist/maintenance/plans.d.ts +28 -1
- package/dist/maintenance/plans.d.ts.map +1 -1
- package/dist/maintenance/plans.js +360 -17
- package/dist/maintenance/plans.js.map +1 -1
- package/dist/memory/intent.d.ts +22 -0
- package/dist/memory/intent.d.ts.map +1 -0
- package/dist/memory/intent.js +59 -0
- package/dist/memory/intent.js.map +1 -0
- package/dist/memory/projection.d.ts +23 -0
- package/dist/memory/projection.d.ts.map +1 -0
- package/dist/memory/projection.js +151 -0
- package/dist/memory/projection.js.map +1 -0
- package/dist/models/client.d.ts.map +1 -1
- package/dist/models/client.js +8 -3
- package/dist/models/client.js.map +1 -1
- package/dist/models/provider-api.d.ts.map +1 -1
- package/dist/models/provider-api.js +4 -2
- package/dist/models/provider-api.js.map +1 -1
- package/dist/models/provider-request.d.ts +22 -0
- package/dist/models/provider-request.d.ts.map +1 -0
- package/dist/models/provider-request.js +95 -0
- package/dist/models/provider-request.js.map +1 -0
- package/dist/observations/marker.d.ts +27 -0
- package/dist/observations/marker.d.ts.map +1 -0
- package/dist/observations/marker.js +238 -0
- package/dist/observations/marker.js.map +1 -0
- package/dist/observations/projection.d.ts +23 -0
- package/dist/observations/projection.d.ts.map +1 -0
- package/dist/observations/projection.js +349 -0
- package/dist/observations/projection.js.map +1 -0
- package/dist/open.d.ts +3 -0
- package/dist/open.d.ts.map +1 -1
- package/dist/open.js +2 -0
- package/dist/open.js.map +1 -1
- package/dist/ops/answer.d.ts.map +1 -1
- package/dist/ops/answer.js +184 -17
- package/dist/ops/answer.js.map +1 -1
- package/dist/ops/context.d.ts.map +1 -1
- package/dist/ops/context.js +24 -4
- package/dist/ops/context.js.map +1 -1
- package/dist/ops/graph.d.ts.map +1 -1
- package/dist/ops/graph.js +101 -8
- package/dist/ops/graph.js.map +1 -1
- package/dist/ops/read.d.ts.map +1 -1
- package/dist/ops/read.js +9 -3
- package/dist/ops/read.js.map +1 -1
- package/dist/ops/recall.d.ts.map +1 -1
- package/dist/ops/recall.js +120 -69
- package/dist/ops/recall.js.map +1 -1
- package/dist/recall/assemble.d.ts +4 -1
- package/dist/recall/assemble.d.ts.map +1 -1
- package/dist/recall/assemble.js +43 -9
- package/dist/recall/assemble.js.map +1 -1
- package/dist/recall/graph-arm.js +9 -1
- package/dist/recall/graph-arm.js.map +1 -1
- package/dist/recall/search.d.ts.map +1 -1
- package/dist/recall/search.js +2 -1
- package/dist/recall/search.js.map +1 -1
- package/dist/store/db.d.ts.map +1 -1
- package/dist/store/db.js +7 -1
- package/dist/store/db.js.map +1 -1
- package/dist/store/migrations.d.ts +3 -1
- package/dist/store/migrations.d.ts.map +1 -1
- package/dist/store/migrations.js +126 -1
- package/dist/store/migrations.js.map +1 -1
- package/package.json +2 -2
|
@@ -24,6 +24,8 @@ import { verifyDreamRun } from "./run-verification.js";
|
|
|
24
24
|
import { Indexer } from "../index/indexer.js";
|
|
25
25
|
import { planManagedItems } from "./managed-items.js";
|
|
26
26
|
import { planRuleDrifts, ruleDriftPaths } from "./rule-drift.js";
|
|
27
|
+
import { independentProofGroups, insertObservationBlock, observationBlock, observationId, observationMarkerIndexes, renderObservationMarker, replaceObservationBlock, } from "../observations/marker.js";
|
|
28
|
+
import { liveObservationProofGroups, markerFromProjection, proofGroupsForFact, } from "../observations/projection.js";
|
|
27
29
|
import { assertProfileAutomaticApplyAvailable, configWithMaintenanceRecovery, maintenanceRecoveryStatus, recordMaintenanceRecovery, } from "./recovery.js";
|
|
28
30
|
/**
|
|
29
31
|
* Order matters twice.
|
|
@@ -1183,36 +1185,49 @@ async function previewObservePhase(ctx, report) {
|
|
|
1183
1185
|
function asPreviewObservation(entry) {
|
|
1184
1186
|
if (entry.action === 'created')
|
|
1185
1187
|
return { ...entry, action: 'would-create' };
|
|
1188
|
+
if (entry.action === 'reinforced')
|
|
1189
|
+
return { ...entry, action: 'would-reinforce' };
|
|
1186
1190
|
if (entry.action === 'refined')
|
|
1187
1191
|
return { ...entry, action: 'would-refine' };
|
|
1192
|
+
if (entry.action === 'weakened')
|
|
1193
|
+
return { ...entry, action: 'would-weaken' };
|
|
1194
|
+
if (entry.action === 'retracted')
|
|
1195
|
+
return { ...entry, action: 'would-retract' };
|
|
1196
|
+
if (entry.action === 'split')
|
|
1197
|
+
return { ...entry, action: 'would-split' };
|
|
1188
1198
|
return entry;
|
|
1189
1199
|
}
|
|
1190
1200
|
async function collectPreparedObservations(ctx, report) {
|
|
1191
1201
|
const groups = subjectGroups(ctx, ctx.config.maintenance.observe.maxSubjects, report.conflicts);
|
|
1202
|
+
const prepared = await staleObservationOutcomes(ctx);
|
|
1203
|
+
for (const entry of prepared) {
|
|
1204
|
+
if (entry.rejectionReason)
|
|
1205
|
+
report.rejected.push({ pattern: entry.written.pattern, reason: entry.rejectionReason });
|
|
1206
|
+
}
|
|
1192
1207
|
if (groups.length === 0)
|
|
1193
|
-
return
|
|
1194
|
-
const prepared = [];
|
|
1208
|
+
return prepared;
|
|
1195
1209
|
// Gathered once for the whole phase, not per group: fifteen groups reading the same two things
|
|
1196
1210
|
// fifteen times is the same answer at fifteen times the cost.
|
|
1197
1211
|
const knownFacts = liveFactClaims(ctx, report.conflicts);
|
|
1198
|
-
const observationsBySlug =
|
|
1212
|
+
const observationsBySlug = indexedObservations(ctx);
|
|
1199
1213
|
for (const group of groups) {
|
|
1200
1214
|
const result = await runObserveMission({
|
|
1201
|
-
// The
|
|
1202
|
-
//
|
|
1203
|
-
subject: group.
|
|
1204
|
-
facts: group.facts,
|
|
1215
|
+
// The display label comes from the canonical entity page. Group membership itself is the
|
|
1216
|
+
// exact resolved entity id below, never a folder or a model-generated subject string.
|
|
1217
|
+
subject: group.subject,
|
|
1218
|
+
facts: group.facts.map((fact) => ({ id: fact.id, claim: fact.claim, slug: fact.slug })),
|
|
1205
1219
|
model: ctx.models.derive,
|
|
1206
1220
|
mission: ctx.config.maintenance.observe.mission,
|
|
1207
1221
|
minEvidence: ctx.config.maintenance.observe.minEvidence,
|
|
1208
1222
|
// What last night already concluded about this subject. The facts rarely change between one
|
|
1209
1223
|
// night and the next, so without this the same insight comes back reworded every night and
|
|
1210
1224
|
// the page accumulates paraphrases of one sentence.
|
|
1211
|
-
existing: observationsBySlug.get(
|
|
1225
|
+
existing: observationsBySlug.get(group.targetSlug) ?? [],
|
|
1226
|
+
existingRecords: indexedObservationRecords(ctx, group.subjectEntity, group.targetSlug),
|
|
1212
1227
|
// Everything already observed elsewhere, plus anything written earlier in this same run —
|
|
1213
1228
|
// two groups can reach one conclusion from overlapping facts, and each would otherwise write
|
|
1214
1229
|
// it to its own page where neither looks like a duplicate.
|
|
1215
|
-
otherObservations: [...observationsBySlug].flatMap(([slug, lines]) => slug ===
|
|
1230
|
+
otherObservations: [...observationsBySlug].flatMap(([slug, lines]) => slug === group.targetSlug ? [] : lines),
|
|
1216
1231
|
knownFacts,
|
|
1217
1232
|
});
|
|
1218
1233
|
if (result.error) {
|
|
@@ -1221,227 +1236,473 @@ async function collectPreparedObservations(ctx, report) {
|
|
|
1221
1236
|
}
|
|
1222
1237
|
report.rejected.push(...result.rejected);
|
|
1223
1238
|
for (const observation of result.observations) {
|
|
1224
|
-
const outcome = await
|
|
1239
|
+
const outcome = await prepareCoLocatedObservation(ctx, group, observation);
|
|
1225
1240
|
prepared.push(outcome);
|
|
1226
1241
|
if (outcome.rejectionReason) {
|
|
1227
1242
|
report.rejected.push({ pattern: observation.pattern, reason: outcome.rejectionReason });
|
|
1228
1243
|
}
|
|
1229
1244
|
// So a later group in this same run sees it. Without this, cross-page duplicates are caught
|
|
1230
1245
|
// only from the second night onwards — the night they are created, they both go through.
|
|
1231
|
-
|
|
1232
|
-
|
|
1246
|
+
observationsBySlug.set(group.targetSlug, [
|
|
1247
|
+
...(observationsBySlug.get(group.targetSlug) ?? []),
|
|
1248
|
+
observation.pattern,
|
|
1249
|
+
]);
|
|
1233
1250
|
}
|
|
1234
1251
|
}
|
|
1235
1252
|
return prepared;
|
|
1236
1253
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1254
|
+
async function staleObservationOutcomes(ctx) {
|
|
1255
|
+
const rows = ctx.store.db
|
|
1256
|
+
.prepare(`SELECT oe.id, oe.source_slug, oe.payload, oe.payload_hash, oe.subject_entity,
|
|
1257
|
+
oe.proof_count, oe.issue, p.rel_path, p.role, p.observe_management
|
|
1258
|
+
FROM observation_entries oe JOIN pages p ON p.id = oe.source_page
|
|
1259
|
+
WHERE oe.eligible = 0 AND oe.disposition = 'active'
|
|
1260
|
+
ORDER BY oe.source_slug, oe.marker_line`)
|
|
1261
|
+
.all();
|
|
1262
|
+
const prepared = [];
|
|
1263
|
+
for (const row of rows) {
|
|
1264
|
+
const marker = markerFromProjection(ctx.store, row.id);
|
|
1265
|
+
const survivingProofs = marker ? liveObservationProofGroups(ctx.store, marker) : new Set();
|
|
1266
|
+
const outcome = survivingProofs.size > 0 ? 'weaken' : 'retract';
|
|
1267
|
+
const action = outcome === 'weaken' ? 'weakened' : 'retracted';
|
|
1268
|
+
const pattern = row.payload
|
|
1269
|
+
.replace(/^- \*\*Observation:\*\*\s*/, '')
|
|
1270
|
+
.replace(/\s+Evidence:\s+(?:\[\[[^\]]+\]\]\s*)+$/, '')
|
|
1271
|
+
.trim();
|
|
1272
|
+
const written = { slug: row.source_slug, pattern, evidence: [], action };
|
|
1273
|
+
if (!marker || row.role !== 'knowledge' || row.observe_management !== 'integrate' || !row.issue) {
|
|
1274
|
+
prepared.push({
|
|
1275
|
+
written: { ...written, action: outcome === 'weaken' ? 'would-weaken' : 'would-retract' },
|
|
1276
|
+
draft: null,
|
|
1277
|
+
rejectionReason: `held ${outcome}: ${row.issue ?? 'the target no longer admits observation integration'}`,
|
|
1278
|
+
});
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
const absPath = path.join(ctx.config.aknoPath, row.rel_path);
|
|
1282
|
+
const before = await fsp.readFile(absPath, 'utf8').catch(() => null);
|
|
1283
|
+
if (before === null) {
|
|
1284
|
+
prepared.push({
|
|
1285
|
+
written: { ...written, action: outcome === 'weaken' ? 'would-weaken' : 'would-retract' },
|
|
1286
|
+
draft: null,
|
|
1287
|
+
rejectionReason: `held ${outcome}: the target page is no longer readable`,
|
|
1288
|
+
});
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const disposition = outcome === 'weaken' ? 'weakened' : 'retracted';
|
|
1292
|
+
const replacement = `${renderObservationMarker({ ...marker, disposition })}\n${row.payload}`;
|
|
1293
|
+
const after = replaceObservationBlock(before, marker.id, replacement);
|
|
1294
|
+
if (after === null) {
|
|
1295
|
+
prepared.push({
|
|
1296
|
+
written: { ...written, action: outcome === 'weaken' ? 'would-weaken' : 'would-retract' },
|
|
1297
|
+
draft: null,
|
|
1298
|
+
rejectionReason: `held ${outcome}: the owned block no longer matches its projection`,
|
|
1299
|
+
});
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
const sealedEvidence = [];
|
|
1303
|
+
for (const locator of marker.evidence) {
|
|
1304
|
+
const fact = ctx.store.db
|
|
1305
|
+
.prepare(`SELECT p.slug, p.rel_path FROM facts f JOIN pages p ON p.id = f.page_id WHERE f.id = ?`)
|
|
1306
|
+
.get(locator.factId);
|
|
1307
|
+
const sourceBytes = fact
|
|
1308
|
+
? await fsp.readFile(path.join(ctx.config.aknoPath, fact.rel_path), 'utf8').catch(() => null)
|
|
1309
|
+
: null;
|
|
1310
|
+
sealedEvidence.push({
|
|
1311
|
+
slug: fact?.slug ?? `fact:${locator.factId}`,
|
|
1312
|
+
contentHash: sourceBytes === null ? null : sha256(sourceBytes),
|
|
1313
|
+
factId: locator.factId,
|
|
1314
|
+
sourceLineHash: locator.sourceLineHash,
|
|
1315
|
+
proofGroups: locator.proofGroups,
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
const inputHash = sha256(JSON.stringify({
|
|
1319
|
+
target: row.source_slug,
|
|
1320
|
+
beforeHash: sha256(before),
|
|
1321
|
+
observationId: row.id,
|
|
1322
|
+
payloadHash: row.payload_hash,
|
|
1323
|
+
issue: row.issue,
|
|
1324
|
+
outcome,
|
|
1325
|
+
evidence: sealedEvidence,
|
|
1326
|
+
}));
|
|
1327
|
+
prepared.push({
|
|
1328
|
+
written,
|
|
1329
|
+
draft: {
|
|
1330
|
+
slug: row.source_slug,
|
|
1331
|
+
relPath: row.rel_path,
|
|
1332
|
+
inputHash,
|
|
1333
|
+
before,
|
|
1334
|
+
after,
|
|
1335
|
+
evidence: sealedEvidence,
|
|
1336
|
+
observationId: row.id,
|
|
1337
|
+
observationSubject: row.subject_entity,
|
|
1338
|
+
observationDisposition: disposition,
|
|
1339
|
+
observationProofCount: row.proof_count,
|
|
1340
|
+
observationOutcome: outcome,
|
|
1341
|
+
observationTargetId: row.id,
|
|
1342
|
+
observationPayloadHash: row.payload_hash,
|
|
1343
|
+
observationQualificationIssue: row.issue,
|
|
1344
|
+
},
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
return prepared;
|
|
1348
|
+
}
|
|
1349
|
+
/** Live eligible level-one facts grouped only by exact resolved subject identity. */
|
|
1252
1350
|
function subjectGroups(ctx, maxSubjects, conflicts) {
|
|
1253
|
-
const observations = ctx.config.paths.observations;
|
|
1254
1351
|
const ineligible = ineligibleConflictClaims(conflicts);
|
|
1255
1352
|
const rows = ctx.store.db
|
|
1256
|
-
.prepare(`SELECT f.
|
|
1353
|
+
.prepare(`SELECT f.id, f.claim, f.line_start, f.source_line_hash, f.item_id,
|
|
1354
|
+
p.id AS page_id, p.slug, p.updated_at, g.subject_entity,
|
|
1355
|
+
e.canonical_page, cp.slug AS canonical_slug, cp.rel_path AS canonical_rel_path,
|
|
1356
|
+
cp.observe_management AS canonical_observe
|
|
1257
1357
|
FROM facts f JOIN pages p ON p.id = f.page_id
|
|
1358
|
+
JOIN graph_fact_status g ON g.fact_id = f.id
|
|
1359
|
+
JOIN graph_entities e ON e.id = g.subject_entity
|
|
1360
|
+
JOIN pages cp ON cp.id = e.canonical_page
|
|
1258
1361
|
WHERE f.valid_to IS NULL
|
|
1259
|
-
AND f.subject IS NOT NULL
|
|
1260
1362
|
AND f.confidence >= 0.5
|
|
1261
|
-
-- Only canonical claims. A source page is evidence someone else wrote.
|
|
1262
1363
|
AND p.role = 'knowledge'
|
|
1263
1364
|
AND p.derived_hash = p.body_hash
|
|
1264
|
-
|
|
1265
|
-
AND
|
|
1266
|
-
AND p.slug NOT LIKE ?
|
|
1365
|
+
AND g.eligibility = 'eligible'
|
|
1366
|
+
AND g.traversable = 1
|
|
1267
1367
|
ORDER BY p.updated_at DESC`)
|
|
1268
|
-
.all(
|
|
1368
|
+
.all();
|
|
1269
1369
|
const groups = new Map();
|
|
1270
1370
|
for (const row of rows) {
|
|
1271
1371
|
if (ineligible.has(claimKey(row.slug, row.line_start)))
|
|
1272
1372
|
continue;
|
|
1273
|
-
const
|
|
1274
|
-
if (
|
|
1373
|
+
const target = observationTarget(ctx, row);
|
|
1374
|
+
if (!target)
|
|
1375
|
+
continue;
|
|
1376
|
+
const proofs = [...proofGroupsForFact(ctx.store, row.id, row.page_id, row.item_id)].sort();
|
|
1377
|
+
if (proofs.length === 0)
|
|
1275
1378
|
continue;
|
|
1276
|
-
const
|
|
1277
|
-
const key = `${folder}|${subject}`;
|
|
1379
|
+
const key = row.subject_entity;
|
|
1278
1380
|
const existing = groups.get(key);
|
|
1279
1381
|
if (existing) {
|
|
1280
|
-
if (existing.
|
|
1281
|
-
|
|
1382
|
+
if (existing.targetSlug !== target.slug) {
|
|
1383
|
+
groups.delete(key);
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
if (existing.facts.length < 30) {
|
|
1387
|
+
existing.facts.push({
|
|
1388
|
+
id: row.id,
|
|
1389
|
+
claim: row.claim,
|
|
1390
|
+
slug: row.slug,
|
|
1391
|
+
sourceLineHash: row.source_line_hash,
|
|
1392
|
+
proofGroups: proofs,
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1282
1395
|
}
|
|
1283
1396
|
else {
|
|
1284
1397
|
groups.set(key, {
|
|
1285
|
-
subject:
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1398
|
+
subject: target.title,
|
|
1399
|
+
subjectEntity: row.subject_entity,
|
|
1400
|
+
targetSlug: target.slug,
|
|
1401
|
+
targetRelPath: target.relPath,
|
|
1402
|
+
facts: [
|
|
1403
|
+
{
|
|
1404
|
+
id: row.id,
|
|
1405
|
+
claim: row.claim,
|
|
1406
|
+
slug: row.slug,
|
|
1407
|
+
sourceLineHash: row.source_line_hash,
|
|
1408
|
+
proofGroups: proofs,
|
|
1409
|
+
},
|
|
1410
|
+
],
|
|
1289
1411
|
});
|
|
1290
1412
|
}
|
|
1291
1413
|
}
|
|
1292
1414
|
return [...groups.values()]
|
|
1293
|
-
.filter((group) =>
|
|
1415
|
+
.filter((group) => independentProofGroups(group.facts.map((fact) => ({
|
|
1416
|
+
factId: fact.id,
|
|
1417
|
+
sourceLineHash: fact.sourceLineHash,
|
|
1418
|
+
proofGroups: fact.proofGroups,
|
|
1419
|
+
}))).size >= ctx.config.maintenance.observe.minEvidence)
|
|
1294
1420
|
.slice(0, maxSubjects);
|
|
1295
1421
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
* So the page is only ever appended to, and an observation already on it is left exactly as it
|
|
1301
|
-
* is. That is also what makes the phase safe to re-run: a second pass over unchanged facts
|
|
1302
|
-
* reports `unchanged` and writes nothing.
|
|
1303
|
-
*/
|
|
1304
|
-
/** Where a subject's observations live. */
|
|
1305
|
-
function observationSlug(ctx, pageSlug) {
|
|
1306
|
-
return normalizeSlug(`${ctx.config.paths.observations}/${pageSlug}`);
|
|
1307
|
-
}
|
|
1308
|
-
/**
|
|
1309
|
-
* Every observation already written, by page.
|
|
1310
|
-
*
|
|
1311
|
-
* Read from the files rather than the index because the index is a reading of the files and this
|
|
1312
|
-
* runs inside the same cycle that writes them.
|
|
1313
|
-
*/
|
|
1314
|
-
async function allObservations(ctx) {
|
|
1315
|
-
const root = path.join(ctx.config.aknoPath, ctx.config.paths.observations);
|
|
1316
|
-
const bySlug = new Map();
|
|
1317
|
-
const entries = await fsp.readdir(root, { withFileTypes: true, recursive: true }).catch(() => []);
|
|
1318
|
-
for (const entry of entries) {
|
|
1319
|
-
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
1320
|
-
continue;
|
|
1321
|
-
const abs = path.join(entry.parentPath ?? root, entry.name);
|
|
1322
|
-
// Skipped for the same reason `scanTree` and the watcher skip them: a dot path is
|
|
1323
|
-
// sync-client or editor state, not an observation. Checked on the whole relative
|
|
1324
|
-
// path rather than `entry.name`, because a recursive readdir reports a file inside
|
|
1325
|
-
// `.trash/` with a perfectly ordinary basename. It also has to happen before
|
|
1326
|
-
// `normalizeSlug` below, which now refuses a dot segment — an unfiltered walk would
|
|
1327
|
-
// turn one stray `.backup.md` into a throw inside the nightly cycle.
|
|
1328
|
-
if (path
|
|
1329
|
-
.relative(root, abs)
|
|
1330
|
-
.split(path.sep)
|
|
1331
|
-
.some((segment) => segment.startsWith('.'))) {
|
|
1332
|
-
continue;
|
|
1333
|
-
}
|
|
1334
|
-
const body = await fsp.readFile(abs, 'utf8').catch(() => null);
|
|
1335
|
-
if (body === null)
|
|
1336
|
-
continue;
|
|
1337
|
-
const lines = body
|
|
1338
|
-
.split('\n')
|
|
1339
|
-
.filter((line) => /^- \d{4}-\d{2}-\d{2} — /.test(line))
|
|
1340
|
-
.map((line) => line
|
|
1341
|
-
.replace(/^- \d{4}-\d{2}-\d{2} — /, '')
|
|
1342
|
-
.replace(/\s*\[\[[^\]]*\]\]/g, '')
|
|
1343
|
-
.trim())
|
|
1344
|
-
.filter(Boolean);
|
|
1345
|
-
if (lines.length === 0)
|
|
1346
|
-
continue;
|
|
1347
|
-
const relative = path.relative(ctx.config.aknoPath, abs).replace(/\.md$/, '');
|
|
1348
|
-
bySlug.set(normalizeSlug(relative), lines);
|
|
1422
|
+
function observationTarget(ctx, row) {
|
|
1423
|
+
if (row.canonical_observe === 'integrate') {
|
|
1424
|
+
const page = ctx.store.db.prepare('SELECT title FROM pages WHERE id = ?').get(row.canonical_page);
|
|
1425
|
+
return page ? { slug: row.canonical_slug, relPath: row.canonical_rel_path, title: page.title } : null;
|
|
1349
1426
|
}
|
|
1350
|
-
|
|
1427
|
+
const topics = ctx.store.db
|
|
1428
|
+
.prepare(`SELECT slug, rel_path, title, about FROM pages
|
|
1429
|
+
WHERE role = 'knowledge' AND observe_management = 'integrate' AND id != ?`)
|
|
1430
|
+
.all(row.canonical_page).filter((page) => JSON.parse(page.about).includes(row.canonical_slug));
|
|
1431
|
+
return topics.length === 1
|
|
1432
|
+
? { slug: topics[0].slug, relPath: topics[0].rel_path, title: topics[0].title }
|
|
1433
|
+
: null;
|
|
1351
1434
|
}
|
|
1352
|
-
|
|
1353
|
-
* Every live claim in the knowledge base, so an "observation" cannot be one of them handed back.
|
|
1354
|
-
*
|
|
1355
|
-
* Superseded claims are left out: their sentence has already been replaced, and an observation is
|
|
1356
|
-
* not a repeat of something the knowledge base no longer says.
|
|
1357
|
-
*/
|
|
1358
|
-
function liveFactClaims(ctx, conflicts = []) {
|
|
1359
|
-
const ineligible = ineligibleConflictClaims(conflicts);
|
|
1435
|
+
function indexedObservations(ctx) {
|
|
1360
1436
|
const rows = ctx.store.db
|
|
1361
|
-
.prepare(`SELECT
|
|
1362
|
-
|
|
1363
|
-
WHERE f.valid_to IS NULL AND p.derived_hash = p.body_hash`)
|
|
1437
|
+
.prepare(`SELECT source_slug, payload FROM observation_entries
|
|
1438
|
+
WHERE disposition != 'retracted' ORDER BY source_slug, marker_line`)
|
|
1364
1439
|
.all();
|
|
1365
|
-
|
|
1440
|
+
const result = new Map();
|
|
1441
|
+
for (const row of rows) {
|
|
1442
|
+
const pattern = row.payload
|
|
1443
|
+
.replace(/^- \*\*Observation:\*\*\s*/, '')
|
|
1444
|
+
.replace(/\s+Evidence:\s+(?:\[\[[^\]]+\]\]\s*)+$/, '')
|
|
1445
|
+
.trim();
|
|
1446
|
+
const bucket = result.get(row.source_slug);
|
|
1447
|
+
if (bucket)
|
|
1448
|
+
bucket.push(pattern);
|
|
1449
|
+
else
|
|
1450
|
+
result.set(row.source_slug, [pattern]);
|
|
1451
|
+
}
|
|
1452
|
+
return result;
|
|
1366
1453
|
}
|
|
1367
|
-
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1454
|
+
function indexedObservationRecords(ctx, subject, slug) {
|
|
1455
|
+
const rows = ctx.store.db
|
|
1456
|
+
.prepare(`SELECT id, payload FROM observation_entries
|
|
1457
|
+
WHERE subject_entity = ? AND source_slug = ? AND disposition = 'active' AND eligible = 1
|
|
1458
|
+
ORDER BY marker_line`)
|
|
1459
|
+
.all(subject, slug);
|
|
1460
|
+
return rows.map((row) => ({
|
|
1461
|
+
id: row.id,
|
|
1462
|
+
pattern: row.payload
|
|
1463
|
+
.replace(/^- \*\*Observation:\*\*\s*/, '')
|
|
1464
|
+
.replace(/\s+Evidence:\s+(?:\[\[[^\]]+\]\]\s*)+$/, '')
|
|
1465
|
+
.trim(),
|
|
1466
|
+
}));
|
|
1467
|
+
}
|
|
1468
|
+
async function prepareCoLocatedObservation(ctx, group, observation) {
|
|
1469
|
+
let selected = observation.evidence.flatMap((id) => {
|
|
1470
|
+
const fact = group.facts.find((candidate) => candidate.id === id);
|
|
1471
|
+
return fact ? [fact] : [];
|
|
1472
|
+
});
|
|
1473
|
+
const outcome = observation.outcome ?? 'create';
|
|
1474
|
+
const targetMarker = observation.targetId ? markerFromProjection(ctx.store, observation.targetId) : null;
|
|
1475
|
+
const targetRow = observation.targetId
|
|
1476
|
+
? ctx.store.db
|
|
1477
|
+
.prepare(`SELECT source_slug, payload FROM observation_entries
|
|
1478
|
+
WHERE id = ? AND subject_entity = ? AND disposition = 'active' AND eligible = 1`)
|
|
1479
|
+
.get(observation.targetId, group.subjectEntity)
|
|
1480
|
+
: undefined;
|
|
1481
|
+
if (outcome !== 'create' && (!targetMarker || !targetRow || targetRow.source_slug !== group.targetSlug)) {
|
|
1377
1482
|
return {
|
|
1378
|
-
written: {
|
|
1483
|
+
written: {
|
|
1484
|
+
slug: group.targetSlug,
|
|
1485
|
+
pattern: observation.pattern,
|
|
1486
|
+
evidence: [],
|
|
1487
|
+
action: 'rejected',
|
|
1488
|
+
},
|
|
1379
1489
|
draft: null,
|
|
1490
|
+
rejectionReason: 'the requested observation lifecycle target is no longer active and eligible',
|
|
1380
1491
|
};
|
|
1381
1492
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1493
|
+
if (outcome === 'reinforce' && targetMarker) {
|
|
1494
|
+
selected = await observationFactsForLocators(ctx, [
|
|
1495
|
+
...targetMarker.evidence,
|
|
1496
|
+
...selected.map((fact) => ({
|
|
1497
|
+
factId: fact.id,
|
|
1498
|
+
sourceLineHash: fact.sourceLineHash,
|
|
1499
|
+
proofGroups: fact.proofGroups,
|
|
1500
|
+
})),
|
|
1501
|
+
]);
|
|
1502
|
+
}
|
|
1503
|
+
if (selected.length > 12) {
|
|
1384
1504
|
return {
|
|
1385
|
-
written: {
|
|
1505
|
+
written: {
|
|
1506
|
+
slug: group.targetSlug,
|
|
1507
|
+
pattern: observation.pattern,
|
|
1508
|
+
evidence: [],
|
|
1509
|
+
action: 'rejected',
|
|
1510
|
+
},
|
|
1386
1511
|
draft: null,
|
|
1512
|
+
rejectionReason: 'observation lineage exceeds the 12-fact marker limit',
|
|
1387
1513
|
};
|
|
1388
1514
|
}
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1515
|
+
const locators = selected.map((fact) => ({
|
|
1516
|
+
factId: fact.id,
|
|
1517
|
+
sourceLineHash: fact.sourceLineHash,
|
|
1518
|
+
proofGroups: fact.proofGroups,
|
|
1519
|
+
}));
|
|
1520
|
+
const proofCount = independentProofGroups(locators).size;
|
|
1521
|
+
const action = outcome === 'reinforce'
|
|
1522
|
+
? 'reinforced'
|
|
1523
|
+
: outcome === 'refine'
|
|
1524
|
+
? 'refined'
|
|
1525
|
+
: outcome === 'split'
|
|
1526
|
+
? 'split'
|
|
1527
|
+
: 'created';
|
|
1396
1528
|
const written = {
|
|
1397
|
-
slug,
|
|
1529
|
+
slug: group.targetSlug,
|
|
1398
1530
|
pattern: observation.pattern,
|
|
1399
|
-
evidence:
|
|
1400
|
-
action
|
|
1531
|
+
evidence: [...new Set(selected.map((fact) => fact.slug))],
|
|
1532
|
+
action,
|
|
1533
|
+
};
|
|
1534
|
+
if (selected.length < 2 || proofCount < ctx.config.maintenance.observe.minEvidence) {
|
|
1535
|
+
return {
|
|
1536
|
+
written: { ...written, action: 'rejected' },
|
|
1537
|
+
draft: null,
|
|
1538
|
+
rejectionReason: `evidence has ${proofCount} independent proof group(s)`,
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
const absPath = path.join(ctx.config.aknoPath, group.targetRelPath);
|
|
1542
|
+
const before = await fsp.readFile(absPath, 'utf8').catch(() => null);
|
|
1543
|
+
if (before === null) {
|
|
1544
|
+
return {
|
|
1545
|
+
written: { ...written, action: 'rejected' },
|
|
1546
|
+
draft: null,
|
|
1547
|
+
rejectionReason: 'the admitted target page is no longer readable',
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
const existingPattern = targetRow
|
|
1551
|
+
? targetRow.payload
|
|
1552
|
+
.replace(/^- \*\*Observation:\*\*\s*/, '')
|
|
1553
|
+
.replace(/\s+Evidence:\s+(?:\[\[[^\]]+\]\]\s*)+$/, '')
|
|
1554
|
+
.trim()
|
|
1555
|
+
: null;
|
|
1556
|
+
const finalPattern = outcome === 'reinforce' && existingPattern ? existingPattern : observation.pattern;
|
|
1557
|
+
const marker = {
|
|
1558
|
+
id: targetMarker?.id ?? observationId(group.subjectEntity, finalPattern),
|
|
1559
|
+
subject: group.subjectEntity,
|
|
1560
|
+
disposition: 'active',
|
|
1561
|
+
evidence: locators,
|
|
1562
|
+
proofCount,
|
|
1401
1563
|
};
|
|
1402
|
-
|
|
1564
|
+
const block = observationBlock(marker, finalPattern, selected.map((fact) => fact.slug));
|
|
1565
|
+
if (outcome === 'create' && observationMarkerIndexes(before.split(/\r?\n/), marker.id, true).length > 0) {
|
|
1566
|
+
return { written: { ...written, action: 'unchanged' }, draft: null };
|
|
1567
|
+
}
|
|
1568
|
+
let after;
|
|
1569
|
+
if (outcome === 'create') {
|
|
1570
|
+
after = insertObservationBlock(before, block);
|
|
1571
|
+
}
|
|
1572
|
+
else if (outcome === 'split' && targetMarker && existingPattern && observation.splitPattern) {
|
|
1573
|
+
const oldFacts = await observationFactsForLocators(ctx, targetMarker.evidence);
|
|
1574
|
+
if (oldFacts.length !== targetMarker.evidence.length)
|
|
1575
|
+
after = null;
|
|
1576
|
+
else {
|
|
1577
|
+
const superseded = observationBlock({ ...targetMarker, disposition: 'superseded' }, existingPattern, oldFacts.map((fact) => fact.slug));
|
|
1578
|
+
const first = {
|
|
1579
|
+
...marker,
|
|
1580
|
+
id: observationId(group.subjectEntity, observation.pattern),
|
|
1581
|
+
};
|
|
1582
|
+
const second = {
|
|
1583
|
+
...marker,
|
|
1584
|
+
id: observationId(group.subjectEntity, observation.splitPattern),
|
|
1585
|
+
};
|
|
1586
|
+
after = replaceObservationBlock(before, targetMarker.id, `${superseded}\n\n${observationBlock(first, observation.pattern, selected.map((fact) => fact.slug))}\n\n${observationBlock(second, observation.splitPattern, selected.map((fact) => fact.slug))}`);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
after = replaceObservationBlock(before, marker.id, block);
|
|
1591
|
+
}
|
|
1592
|
+
if (after === null) {
|
|
1403
1593
|
return {
|
|
1404
1594
|
written: { ...written, action: 'rejected' },
|
|
1405
1595
|
draft: null,
|
|
1406
|
-
rejectionReason: 'the
|
|
1596
|
+
rejectionReason: 'the owned observation block changed before its lifecycle update was sealed',
|
|
1407
1597
|
};
|
|
1408
1598
|
}
|
|
1409
|
-
const sealedEvidence = await
|
|
1410
|
-
if (sealedEvidence.length !==
|
|
1599
|
+
const sealedEvidence = await sealObservationFacts(ctx, selected);
|
|
1600
|
+
if (sealedEvidence.length !== selected.length) {
|
|
1411
1601
|
return {
|
|
1412
1602
|
written: { ...written, action: 'rejected' },
|
|
1413
1603
|
draft: null,
|
|
1414
|
-
rejectionReason: 'evidence changed
|
|
1604
|
+
rejectionReason: 'exact evidence changed before the observation was sealed',
|
|
1415
1605
|
};
|
|
1416
1606
|
}
|
|
1417
1607
|
const inputHash = sha256(JSON.stringify({
|
|
1418
|
-
|
|
1419
|
-
beforeHash:
|
|
1420
|
-
|
|
1608
|
+
target: group.targetSlug,
|
|
1609
|
+
beforeHash: sha256(before),
|
|
1610
|
+
observation: marker,
|
|
1421
1611
|
evidence: sealedEvidence,
|
|
1422
1612
|
}));
|
|
1423
1613
|
return {
|
|
1424
1614
|
written,
|
|
1425
|
-
draft: {
|
|
1615
|
+
draft: {
|
|
1616
|
+
slug: group.targetSlug,
|
|
1617
|
+
relPath: group.targetRelPath,
|
|
1618
|
+
inputHash,
|
|
1619
|
+
before,
|
|
1620
|
+
after,
|
|
1621
|
+
evidence: sealedEvidence,
|
|
1622
|
+
observationId: marker.id,
|
|
1623
|
+
observationSubject: marker.subject,
|
|
1624
|
+
observationDisposition: marker.disposition,
|
|
1625
|
+
observationProofCount: marker.proofCount,
|
|
1626
|
+
observationOutcome: outcome,
|
|
1627
|
+
...(targetMarker ? { observationTargetId: targetMarker.id } : {}),
|
|
1628
|
+
},
|
|
1426
1629
|
};
|
|
1427
1630
|
}
|
|
1428
|
-
async function
|
|
1631
|
+
async function observationFactsForLocators(ctx, locators) {
|
|
1632
|
+
const byFact = new Map();
|
|
1633
|
+
for (const locator of locators) {
|
|
1634
|
+
if (byFact.has(locator.factId))
|
|
1635
|
+
continue;
|
|
1636
|
+
const row = ctx.store.db
|
|
1637
|
+
.prepare(`SELECT f.id, f.claim, f.source_line_hash, f.item_id, f.page_id, p.slug
|
|
1638
|
+
FROM facts f JOIN pages p ON p.id = f.page_id
|
|
1639
|
+
WHERE f.id = ? AND f.valid_to IS NULL`)
|
|
1640
|
+
.get(locator.factId);
|
|
1641
|
+
if (!row || row.source_line_hash !== locator.sourceLineHash)
|
|
1642
|
+
continue;
|
|
1643
|
+
const proofGroups = [...proofGroupsForFact(ctx.store, row.id, row.page_id, row.item_id)].sort();
|
|
1644
|
+
if (proofGroups.length !== locator.proofGroups.length ||
|
|
1645
|
+
proofGroups.some((proof) => !locator.proofGroups.includes(proof))) {
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
byFact.set(row.id, {
|
|
1649
|
+
id: row.id,
|
|
1650
|
+
claim: row.claim,
|
|
1651
|
+
slug: row.slug,
|
|
1652
|
+
sourceLineHash: row.source_line_hash,
|
|
1653
|
+
proofGroups,
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
return [...byFact.values()];
|
|
1657
|
+
}
|
|
1658
|
+
async function sealObservationFacts(ctx, facts) {
|
|
1429
1659
|
const out = [];
|
|
1430
|
-
for (const
|
|
1660
|
+
for (const fact of facts) {
|
|
1431
1661
|
const row = ctx.store.db
|
|
1432
|
-
.prepare(
|
|
1433
|
-
|
|
1434
|
-
|
|
1662
|
+
.prepare(`SELECT f.source_line_hash, f.valid_to, p.rel_path, p.role, p.derived_hash, p.body_hash
|
|
1663
|
+
FROM facts f JOIN pages p ON p.id = f.page_id WHERE f.id = ?`)
|
|
1664
|
+
.get(fact.id);
|
|
1665
|
+
if (!row ||
|
|
1666
|
+
row.valid_to !== null ||
|
|
1667
|
+
row.source_line_hash !== fact.sourceLineHash ||
|
|
1668
|
+
row.role !== 'knowledge' ||
|
|
1669
|
+
row.derived_hash !== row.body_hash) {
|
|
1435
1670
|
continue;
|
|
1671
|
+
}
|
|
1436
1672
|
const content = await fsp
|
|
1437
1673
|
.readFile(path.join(ctx.config.aknoPath, row.rel_path), 'utf8')
|
|
1438
1674
|
.catch(() => null);
|
|
1439
1675
|
if (content === null)
|
|
1440
1676
|
continue;
|
|
1441
|
-
out.push({
|
|
1677
|
+
out.push({
|
|
1678
|
+
slug: fact.slug,
|
|
1679
|
+
contentHash: sha256(content),
|
|
1680
|
+
factId: fact.id,
|
|
1681
|
+
sourceLineHash: fact.sourceLineHash,
|
|
1682
|
+
proofGroups: fact.proofGroups,
|
|
1683
|
+
});
|
|
1442
1684
|
}
|
|
1443
1685
|
return out;
|
|
1444
1686
|
}
|
|
1687
|
+
/** The configured inference namespace remains the home of the separate L3 principles page. */
|
|
1688
|
+
function inferenceSlug(ctx, pageSlug) {
|
|
1689
|
+
return normalizeSlug(`${ctx.config.paths.observations}/${pageSlug}`);
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Every live claim in the knowledge base, so an "observation" cannot be one of them handed back.
|
|
1693
|
+
*
|
|
1694
|
+
* Superseded claims are left out: their sentence has already been replaced, and an observation is
|
|
1695
|
+
* not a repeat of something the knowledge base no longer says.
|
|
1696
|
+
*/
|
|
1697
|
+
function liveFactClaims(ctx, conflicts = []) {
|
|
1698
|
+
const ineligible = ineligibleConflictClaims(conflicts);
|
|
1699
|
+
const rows = ctx.store.db
|
|
1700
|
+
.prepare(`SELECT f.claim, f.line_start, p.slug
|
|
1701
|
+
FROM facts f JOIN pages p ON p.id = f.page_id
|
|
1702
|
+
WHERE f.valid_to IS NULL AND p.derived_hash = p.body_hash`)
|
|
1703
|
+
.all();
|
|
1704
|
+
return rows.filter((row) => !ineligible.has(claimKey(row.slug, row.line_start))).map((row) => row.claim);
|
|
1705
|
+
}
|
|
1445
1706
|
function inferenceWasRejected(ctx, kind, draft) {
|
|
1446
1707
|
const row = ctx.store.db
|
|
1447
1708
|
.prepare(`SELECT 1 FROM maintenance_items
|
|
@@ -1455,6 +1716,48 @@ function observationFromPlanItem(item) {
|
|
|
1455
1716
|
if (operation.type === 'move')
|
|
1456
1717
|
throw new Error('an inference item cannot move a document');
|
|
1457
1718
|
const after = operation.type === 'delete' ? operation.before : operation.after;
|
|
1719
|
+
if (item.kind === 'observe') {
|
|
1720
|
+
const metadata = item.evidence.find((entry) => entry.observationId);
|
|
1721
|
+
const projectedId = metadata?.observationId;
|
|
1722
|
+
const lines = after.split(/\r?\n/);
|
|
1723
|
+
const marker = projectedId ? (observationMarkerIndexes(lines, projectedId, true)[0] ?? -1) : -1;
|
|
1724
|
+
const payload = marker >= 0 ? (lines[marker + 1] ?? '') : '';
|
|
1725
|
+
const pattern = /^- \*\*Observation:\*\* (.+?) Evidence: /.exec(payload)?.[1] ?? '';
|
|
1726
|
+
const outcome = metadata?.observationOutcome ?? 'create';
|
|
1727
|
+
const proposed = outcome === 'reinforce'
|
|
1728
|
+
? 'would-reinforce'
|
|
1729
|
+
: outcome === 'refine'
|
|
1730
|
+
? 'would-refine'
|
|
1731
|
+
: outcome === 'split'
|
|
1732
|
+
? 'would-split'
|
|
1733
|
+
: outcome === 'weaken'
|
|
1734
|
+
? 'would-weaken'
|
|
1735
|
+
: outcome === 'retract'
|
|
1736
|
+
? 'would-retract'
|
|
1737
|
+
: 'would-create';
|
|
1738
|
+
const completed = outcome === 'reinforce'
|
|
1739
|
+
? 'reinforced'
|
|
1740
|
+
: outcome === 'refine'
|
|
1741
|
+
? 'refined'
|
|
1742
|
+
: outcome === 'split'
|
|
1743
|
+
? 'split'
|
|
1744
|
+
: outcome === 'weaken'
|
|
1745
|
+
? 'weakened'
|
|
1746
|
+
: outcome === 'retract'
|
|
1747
|
+
? 'retracted'
|
|
1748
|
+
: 'created';
|
|
1749
|
+
const action = item.status === 'applied'
|
|
1750
|
+
? completed
|
|
1751
|
+
: ['rejected', 'blocked', 'stale', 'verification_failed'].includes(item.status)
|
|
1752
|
+
? 'rejected'
|
|
1753
|
+
: proposed;
|
|
1754
|
+
return {
|
|
1755
|
+
slug: item.subject,
|
|
1756
|
+
pattern,
|
|
1757
|
+
evidence: [...new Set(item.evidence.map((entry) => entry.source))],
|
|
1758
|
+
action,
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1458
1761
|
const lastLine = after.trimEnd().split('\n').at(-1) ?? '';
|
|
1459
1762
|
const pattern = /^- \d{4}-\d{2}-\d{2} — (.+?)(?:\s+\[\[[^\]]+\]\])+$/.exec(lastLine)?.[1] ?? '';
|
|
1460
1763
|
const proposed = operation.type === 'create' ? 'would-create' : 'would-refine';
|
|
@@ -1472,12 +1775,8 @@ function observationFromPlanItem(item) {
|
|
|
1472
1775
|
action,
|
|
1473
1776
|
};
|
|
1474
1777
|
}
|
|
1475
|
-
/**
|
|
1476
|
-
|
|
1477
|
-
* authors. They are what makes an inference identifiable as one after the fact — by a reader,
|
|
1478
|
-
* by recall's ranking, and by the guard that refuses to feed observations back in.
|
|
1479
|
-
*/
|
|
1480
|
-
function newObservationPage(subject, observation, today) {
|
|
1778
|
+
/** L3 principles retain the legacy inference-page envelope; L2 observations do not use this writer. */
|
|
1779
|
+
function newPrinciplesPage(subject, observation, today) {
|
|
1481
1780
|
const title = subject.charAt(0).toUpperCase() + subject.slice(1);
|
|
1482
1781
|
return (`---\ntitle: ${serializeYamlString(title, 'title')}\nderived: true\n` +
|
|
1483
1782
|
`evidence: ${serializeYamlStringArray(observation.evidence, 'evidence')}\n---\n\n` +
|
|
@@ -1486,7 +1785,7 @@ function newObservationPage(subject, observation, today) {
|
|
|
1486
1785
|
`- ${today} — ${observation.pattern} ${citation(observation.evidence)}\n`);
|
|
1487
1786
|
}
|
|
1488
1787
|
/** Appends the new line and unions the evidence, leaving every existing line alone. */
|
|
1489
|
-
function
|
|
1788
|
+
function appendPrinciple(current, line, evidence) {
|
|
1490
1789
|
const merged = mergeEvidence(current, evidence);
|
|
1491
1790
|
if (merged === null)
|
|
1492
1791
|
return null;
|
|
@@ -1502,7 +1801,7 @@ function mergeEvidence(current, evidence) {
|
|
|
1502
1801
|
return null;
|
|
1503
1802
|
const front = match[1];
|
|
1504
1803
|
// An existing key that is not one exact string sequence is user-edited or malformed. Refuse
|
|
1505
|
-
// to guess around it and propagate that refusal so no unsupported
|
|
1804
|
+
// to guess around it and propagate that refusal so no unsupported principle is appended.
|
|
1506
1805
|
if (/^evidence:/m.test(front))
|
|
1507
1806
|
return null;
|
|
1508
1807
|
// Add the owned key without round-tripping any neighboring frontmatter.
|
|
@@ -1513,13 +1812,6 @@ function mergeEvidence(current, evidence) {
|
|
|
1513
1812
|
function citation(evidence) {
|
|
1514
1813
|
return evidence.map((slug) => `[[${slug}]]`).join(' ');
|
|
1515
1814
|
}
|
|
1516
|
-
function slugify(subject) {
|
|
1517
|
-
return (subject
|
|
1518
|
-
.toLowerCase()
|
|
1519
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
1520
|
-
.replace(/^-+|-+$/g, '')
|
|
1521
|
-
.slice(0, 60) || 'pattern');
|
|
1522
|
-
}
|
|
1523
1815
|
// ─── Reflect ────────────────────────────────────────────────────────────────
|
|
1524
1816
|
/**
|
|
1525
1817
|
* The third tier — **off by default**, and shipped as an extension point rather than a
|
|
@@ -1587,25 +1879,23 @@ async function previewReflectPhase(ctx, report) {
|
|
|
1587
1879
|
}
|
|
1588
1880
|
}
|
|
1589
1881
|
async function collectPreparedReflections(ctx, report) {
|
|
1590
|
-
// The page
|
|
1591
|
-
//
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
const target = observationSlug(ctx, PRINCIPLES_SLUG);
|
|
1595
|
-
const observations = await allObservations(ctx);
|
|
1596
|
-
const ineligibleSources = ineligibleConflictSourceSlugs(report.conflicts);
|
|
1882
|
+
// The L3 page must not feed its own prior conclusions back into reflection. L2 input comes only
|
|
1883
|
+
// from eligible projected markers, while the separate principles page remains write-only here.
|
|
1884
|
+
const target = inferenceSlug(ctx, PRINCIPLES_SLUG);
|
|
1885
|
+
const observations = indexedObservations(ctx);
|
|
1597
1886
|
const rows = ctx.store.db
|
|
1598
|
-
.prepare(`SELECT slug,
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1887
|
+
.prepare(`SELECT oe.id, oe.source_slug AS slug, oe.payload, oe.payload_hash, p.rel_path
|
|
1888
|
+
FROM observation_entries oe JOIN pages p ON p.id = oe.source_page
|
|
1889
|
+
WHERE oe.eligible = 1 AND oe.disposition = 'active'
|
|
1890
|
+
ORDER BY p.updated_at DESC, oe.marker_line DESC LIMIT 40`)
|
|
1891
|
+
.all();
|
|
1602
1892
|
if (rows.length < 2) {
|
|
1603
1893
|
report.warnings.push('reflect had fewer than two observations to build on — nothing was written');
|
|
1604
1894
|
return [];
|
|
1605
1895
|
}
|
|
1606
1896
|
const result = await runObserveMission({
|
|
1607
1897
|
subject: 'decision principles',
|
|
1608
|
-
facts: rows.map((row) => ({ claim: row.
|
|
1898
|
+
facts: rows.map((row) => ({ id: row.id, claim: row.payload, slug: row.slug })),
|
|
1609
1899
|
model: ctx.models.derive,
|
|
1610
1900
|
mission: ctx.config.maintenance.reflect.mission ??
|
|
1611
1901
|
'State durable decision principles and long-term tendencies, not individual patterns.',
|
|
@@ -1613,7 +1903,7 @@ async function collectPreparedReflections(ctx, report) {
|
|
|
1613
1903
|
minEvidence: Math.max(3, ctx.config.maintenance.observe.minEvidence),
|
|
1614
1904
|
// This tier appends to one page every night from observations that rarely change, so without
|
|
1615
1905
|
// its own previous answers it restates them — the same way `observe` did, one tier up.
|
|
1616
|
-
existing:
|
|
1906
|
+
existing: await recordedPrinciples(ctx),
|
|
1617
1907
|
// A principle that is one of the observation lines verbatim is not a tier above them. Its
|
|
1618
1908
|
// sources here are page *summaries*, so the lines themselves are not otherwise checked.
|
|
1619
1909
|
otherObservations: [...observations].flatMap(([slug, lines]) => (slug === target ? [] : lines)),
|
|
@@ -1628,7 +1918,7 @@ async function collectPreparedReflections(ctx, report) {
|
|
|
1628
1918
|
report.rejected.push(...result.rejected);
|
|
1629
1919
|
const prepared = [];
|
|
1630
1920
|
for (const observation of result.observations) {
|
|
1631
|
-
const outcome = await
|
|
1921
|
+
const outcome = await prepareIndexedReflection(ctx, observation, rows);
|
|
1632
1922
|
prepared.push(outcome);
|
|
1633
1923
|
if (outcome.rejectionReason) {
|
|
1634
1924
|
report.rejected.push({ pattern: observation.pattern, reason: outcome.rejectionReason });
|
|
@@ -1636,27 +1926,86 @@ async function collectPreparedReflections(ctx, report) {
|
|
|
1636
1926
|
}
|
|
1637
1927
|
return prepared;
|
|
1638
1928
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1929
|
+
/** L3 is one explicit synthesis page, so read only its owned dated entries. */
|
|
1930
|
+
async function recordedPrinciples(ctx) {
|
|
1931
|
+
const relPath = `${inferenceSlug(ctx, PRINCIPLES_SLUG)}.md`;
|
|
1932
|
+
const body = await fsp.readFile(path.join(ctx.config.aknoPath, relPath), 'utf8').catch(() => null);
|
|
1933
|
+
if (body === null)
|
|
1934
|
+
return [];
|
|
1935
|
+
return body
|
|
1936
|
+
.split(/\r?\n/)
|
|
1937
|
+
.filter((line) => /^- \d{4}-\d{2}-\d{2} — /.test(line))
|
|
1938
|
+
.map((line) => line
|
|
1939
|
+
.replace(/^- \d{4}-\d{2}-\d{2} — /, '')
|
|
1940
|
+
.replace(/\s*\[\[[^\]]*\]\]/g, '')
|
|
1941
|
+
.trim())
|
|
1942
|
+
.filter(Boolean);
|
|
1649
1943
|
}
|
|
1650
|
-
function
|
|
1651
|
-
|
|
1652
|
-
const
|
|
1653
|
-
return
|
|
1654
|
-
|
|
1655
|
-
|
|
1944
|
+
async function prepareIndexedReflection(ctx, observation, rows) {
|
|
1945
|
+
const selected = observation.evidence.flatMap((id) => {
|
|
1946
|
+
const row = rows.find((candidate) => candidate.id === id);
|
|
1947
|
+
return row ? [row] : [];
|
|
1948
|
+
});
|
|
1949
|
+
const slug = inferenceSlug(ctx, PRINCIPLES_SLUG);
|
|
1950
|
+
const relPath = `${slug}.md`;
|
|
1951
|
+
const absPath = path.join(ctx.config.aknoPath, relPath);
|
|
1952
|
+
const before = await fsp.readFile(absPath, 'utf8').catch(() => null);
|
|
1953
|
+
const evidenceSlugs = [...new Set(selected.map((entry) => entry.slug))];
|
|
1954
|
+
const written = {
|
|
1955
|
+
slug,
|
|
1956
|
+
pattern: observation.pattern,
|
|
1957
|
+
evidence: evidenceSlugs,
|
|
1958
|
+
action: before === null ? 'created' : 'refined',
|
|
1959
|
+
};
|
|
1960
|
+
if (selected.length < Math.max(3, ctx.config.maintenance.observe.minEvidence)) {
|
|
1961
|
+
return {
|
|
1962
|
+
written: { ...written, action: 'rejected' },
|
|
1963
|
+
draft: null,
|
|
1964
|
+
rejectionReason: 'reflection cited too few eligible level-two observations',
|
|
1965
|
+
};
|
|
1656
1966
|
}
|
|
1657
|
-
|
|
1658
|
-
return
|
|
1967
|
+
if (before?.includes(observation.pattern)) {
|
|
1968
|
+
return { written: { ...written, action: 'unchanged' }, draft: null };
|
|
1659
1969
|
}
|
|
1970
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1971
|
+
const line = `- ${today} — ${observation.pattern} ${citation(evidenceSlugs)}`;
|
|
1972
|
+
const pageCandidate = { ...observation, evidence: evidenceSlugs };
|
|
1973
|
+
const after = before === null
|
|
1974
|
+
? newPrinciplesPage('Principles', pageCandidate, today)
|
|
1975
|
+
: appendPrinciple(before, line, evidenceSlugs);
|
|
1976
|
+
if (after === null) {
|
|
1977
|
+
return {
|
|
1978
|
+
written: { ...written, action: 'rejected' },
|
|
1979
|
+
draft: null,
|
|
1980
|
+
rejectionReason: 'the existing principles page has an unsupported evidence declaration',
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
const evidence = [];
|
|
1984
|
+
for (const row of selected) {
|
|
1985
|
+
const bytes = await fsp.readFile(path.join(ctx.config.aknoPath, row.rel_path), 'utf8').catch(() => null);
|
|
1986
|
+
if (bytes === null)
|
|
1987
|
+
continue;
|
|
1988
|
+
evidence.push({
|
|
1989
|
+
slug: row.slug,
|
|
1990
|
+
contentHash: sha256(bytes),
|
|
1991
|
+
reflectionObservationId: row.id,
|
|
1992
|
+
reflectionPayloadHash: row.payload_hash,
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
if (evidence.length !== selected.length) {
|
|
1996
|
+
return {
|
|
1997
|
+
written: { ...written, action: 'rejected' },
|
|
1998
|
+
draft: null,
|
|
1999
|
+
rejectionReason: 'a level-two source changed before reflection was sealed',
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
const inputHash = sha256(JSON.stringify({
|
|
2003
|
+
slug,
|
|
2004
|
+
before: before === null ? null : sha256(before),
|
|
2005
|
+
pattern: observation.pattern,
|
|
2006
|
+
evidence,
|
|
2007
|
+
}));
|
|
2008
|
+
return { written, draft: { slug, relPath, inputHash, before, after, evidence } };
|
|
1660
2009
|
}
|
|
1661
2010
|
/** A journal entry, as the log wants it: which phase, and what the write added. */
|
|
1662
2011
|
function asApplied(phase, file) {
|