@remnic/core 9.69.54 → 9.69.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.js +2 -2
- package/dist/access-authorization-probe.js +3 -3
- package/dist/access-boundary.js +2 -2
- package/dist/access-cli.js +24 -24
- package/dist/access-extraction-force-flush.js +2 -2
- package/dist/access-http-briefing.js +3 -3
- package/dist/access-http-query.js +2 -2
- package/dist/access-http.js +14 -14
- package/dist/access-identity-continuity-surface.js +2 -2
- package/dist/access-lcm-surface.js +2 -2
- package/dist/access-mcp.js +9 -9
- package/dist/access-namespace-preflight.js +2 -2
- package/dist/access-observe-write-surface.js +2 -2
- package/dist/access-operations-batch.js +3 -3
- package/dist/access-operations.js +8 -8
- package/dist/access-recall-concurrency.js +2 -2
- package/dist/access-recall-response.js +2 -2
- package/dist/access-recall-surface.js +2 -2
- package/dist/access-service.js +2 -2
- package/dist/access-surface-catalog.js +4 -4
- package/dist/active-recall.js +2 -2
- package/dist/{chunk-ZADYEZY4.js → chunk-2SIKTXWX.js} +2 -2
- package/dist/{chunk-ZHV6FGVF.js → chunk-3R23DDD3.js} +8 -8
- package/dist/{chunk-EQNZC3BH.js → chunk-3S6URXEH.js} +2 -2
- package/dist/{chunk-BCXZ4BDS.js → chunk-75YZ5VAI.js} +2 -2
- package/dist/{chunk-MXTCV3B5.js → chunk-AHVJ6GLL.js} +2 -2
- package/dist/{chunk-6UEM5EOB.js → chunk-AQ55YKEV.js} +2 -2
- package/dist/{chunk-VMKAA5SU.js → chunk-BSCNBIF4.js} +2 -2
- package/dist/{chunk-M7HFDWOS.js → chunk-FROKVQQY.js} +2 -2
- package/dist/{chunk-A4TQFNRX.js → chunk-GRDRVYXJ.js} +3 -3
- package/dist/{chunk-EYK3UHAP.js → chunk-H6WOFMOV.js} +2 -2
- package/dist/{chunk-SOOWBRMH.js → chunk-IYNCJKBM.js} +8 -8
- package/dist/{chunk-3KJTSCSX.js → chunk-J54B5DGD.js} +5 -4
- package/dist/chunk-J54B5DGD.js.map +1 -0
- package/dist/{chunk-KGY6F4VJ.js → chunk-KIGC55FQ.js} +2 -2
- package/dist/{chunk-KQQEZTH6.js → chunk-KQVBR7BC.js} +2 -2
- package/dist/{chunk-MTJUIIM4.js → chunk-MTWD4KMM.js} +2 -2
- package/dist/{chunk-WJ5KWJLK.js → chunk-NNEWBRUL.js} +2 -2
- package/dist/{chunk-5IKW2VP6.js → chunk-NPX6WMFJ.js} +2 -2
- package/dist/{chunk-Z22M4JCY.js → chunk-NZLFVLLJ.js} +165 -5
- package/dist/chunk-NZLFVLLJ.js.map +1 -0
- package/dist/{chunk-QKCXRS5A.js → chunk-OGXXGHW6.js} +11 -1
- package/dist/{chunk-QKCXRS5A.js.map → chunk-OGXXGHW6.js.map} +1 -1
- package/dist/{chunk-CO7KBRPF.js → chunk-TJNDFNCU.js} +2 -2
- package/dist/{chunk-OKOG5MXF.js → chunk-XZRZJHBW.js} +10 -10
- package/dist/{chunk-HUQMVI3A.js → chunk-ZBYVR5W4.js} +2 -2
- package/dist/{chunk-ZANEMAEV.js → chunk-ZP73EM2X.js} +2 -2
- package/dist/{chunk-IEOBAOA6.js → chunk-ZXQPAYCS.js} +2 -2
- package/dist/cli.js +19 -19
- package/dist/compounding/engine.js +3 -3
- package/dist/config.js +2 -2
- package/dist/external-wiki-access.js +3 -3
- package/dist/index.js +24 -24
- package/dist/memory-browse-operations.js +3 -3
- package/dist/operator-toolkit.js +3 -3
- package/dist/orchestration/maintenance.js +3 -2
- package/dist/orchestrator.js +24 -24
- package/dist/recall-navigation-operations.js +3 -3
- package/dist/recall-navigation.js +2 -2
- package/dist/resume-bundles.js +3 -3
- package/dist/shared-context/manager.d.ts +16 -0
- package/dist/shared-context/manager.js +2 -2
- package/dist/support-passport/index.js +5 -5
- package/package.json +2 -2
- package/src/activity/config.test.ts +19 -0
- package/src/activity/config.ts +3 -4
- package/src/shared-context/governance-wiring.test.ts +153 -0
- package/src/shared-context/manager.ts +104 -3
- package/src/shared-context/staleness.ts +1 -1
- package/dist/chunk-3KJTSCSX.js.map +0 -1
- package/dist/chunk-Z22M4JCY.js.map +0 -1
- /package/dist/{chunk-ZADYEZY4.js.map → chunk-2SIKTXWX.js.map} +0 -0
- /package/dist/{chunk-ZHV6FGVF.js.map → chunk-3R23DDD3.js.map} +0 -0
- /package/dist/{chunk-EQNZC3BH.js.map → chunk-3S6URXEH.js.map} +0 -0
- /package/dist/{chunk-BCXZ4BDS.js.map → chunk-75YZ5VAI.js.map} +0 -0
- /package/dist/{chunk-MXTCV3B5.js.map → chunk-AHVJ6GLL.js.map} +0 -0
- /package/dist/{chunk-6UEM5EOB.js.map → chunk-AQ55YKEV.js.map} +0 -0
- /package/dist/{chunk-VMKAA5SU.js.map → chunk-BSCNBIF4.js.map} +0 -0
- /package/dist/{chunk-M7HFDWOS.js.map → chunk-FROKVQQY.js.map} +0 -0
- /package/dist/{chunk-A4TQFNRX.js.map → chunk-GRDRVYXJ.js.map} +0 -0
- /package/dist/{chunk-EYK3UHAP.js.map → chunk-H6WOFMOV.js.map} +0 -0
- /package/dist/{chunk-SOOWBRMH.js.map → chunk-IYNCJKBM.js.map} +0 -0
- /package/dist/{chunk-KGY6F4VJ.js.map → chunk-KIGC55FQ.js.map} +0 -0
- /package/dist/{chunk-KQQEZTH6.js.map → chunk-KQVBR7BC.js.map} +0 -0
- /package/dist/{chunk-MTJUIIM4.js.map → chunk-MTWD4KMM.js.map} +0 -0
- /package/dist/{chunk-WJ5KWJLK.js.map → chunk-NNEWBRUL.js.map} +0 -0
- /package/dist/{chunk-5IKW2VP6.js.map → chunk-NPX6WMFJ.js.map} +0 -0
- /package/dist/{chunk-CO7KBRPF.js.map → chunk-TJNDFNCU.js.map} +0 -0
- /package/dist/{chunk-OKOG5MXF.js.map → chunk-XZRZJHBW.js.map} +0 -0
- /package/dist/{chunk-HUQMVI3A.js.map → chunk-ZBYVR5W4.js.map} +0 -0
- /package/dist/{chunk-ZANEMAEV.js.map → chunk-ZP73EM2X.js.map} +0 -0
- /package/dist/{chunk-IEOBAOA6.js.map → chunk-ZXQPAYCS.js.map} +0 -0
package/dist/resume-bundles.js
CHANGED
|
@@ -4,15 +4,15 @@ import {
|
|
|
4
4
|
recordResumeBundle,
|
|
5
5
|
resolveResumeBundleDir,
|
|
6
6
|
validateResumeBundle
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-NNEWBRUL.js";
|
|
8
8
|
import "./chunk-G7NYVKD6.js";
|
|
9
9
|
import "./chunk-AM4T6CHX.js";
|
|
10
10
|
import "./chunk-KFY3SGN7.js";
|
|
11
11
|
import "./chunk-NHLLKEKK.js";
|
|
12
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-KIGC55FQ.js";
|
|
13
13
|
import "./chunk-ZKOPY4WW.js";
|
|
14
14
|
import "./chunk-62OKGIJA.js";
|
|
15
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-J54B5DGD.js";
|
|
16
16
|
import "./chunk-QNJQCLI6.js";
|
|
17
17
|
import "./chunk-IWO6QXWJ.js";
|
|
18
18
|
import "./chunk-WIKO4SCR.js";
|
|
@@ -97,6 +97,22 @@ interface SharedCrossSignalReport {
|
|
|
97
97
|
feedbackEntries: SharedFeedbackEntry[];
|
|
98
98
|
sources: SharedCrossSignalSource[];
|
|
99
99
|
overlaps: SharedCrossSignalOverlap[];
|
|
100
|
+
/** Control 2 (issue #1957): expiry drops and supersession circulation. */
|
|
101
|
+
staleness: {
|
|
102
|
+
expiredDropped: number;
|
|
103
|
+
supersededStillCirculating: string[];
|
|
104
|
+
};
|
|
105
|
+
/** Control 3 (issue #1957): refs two agents ruled on differently. */
|
|
106
|
+
disputed: Array<{
|
|
107
|
+
ref: string;
|
|
108
|
+
agents: string[];
|
|
109
|
+
decisions: string[];
|
|
110
|
+
}>;
|
|
111
|
+
/** Control 4 (issue #1957): who synthesized this report and when. */
|
|
112
|
+
provenance: {
|
|
113
|
+
actor: string;
|
|
114
|
+
at: string;
|
|
115
|
+
};
|
|
100
116
|
semantic: {
|
|
101
117
|
enabled: boolean;
|
|
102
118
|
applied: boolean;
|
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
SharedContextManager,
|
|
3
3
|
SharedFeedbackEntrySchema,
|
|
4
4
|
resolveSharedContextDir
|
|
5
|
-
} from "../chunk-
|
|
6
|
-
import "../chunk-
|
|
5
|
+
} from "../chunk-NZLFVLLJ.js";
|
|
6
|
+
import "../chunk-OGXXGHW6.js";
|
|
7
7
|
import "../chunk-JUOZ62Y7.js";
|
|
8
8
|
import "../chunk-6KH5NLQS.js";
|
|
9
9
|
import "../chunk-P2QRMMVQ.js";
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
SUPPORT_PASSPORT_MODEL_RESULT_PATH,
|
|
5
5
|
SupportPassportModelBridge,
|
|
6
6
|
parseSupportPassportModelJob
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-H6WOFMOV.js";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_SUPPORT_PASSPORT_CONFIG,
|
|
10
10
|
parseSupportPassportConfig
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
buildSupportPassportPublicRequestHandler,
|
|
15
15
|
composeSupportPassportExternalRequestHandlers,
|
|
16
16
|
normalizeSupportPassportNetworkAddress
|
|
17
|
-
} from "../chunk-
|
|
18
|
-
import "../chunk-
|
|
17
|
+
} from "../chunk-2SIKTXWX.js";
|
|
18
|
+
import "../chunk-MTWD4KMM.js";
|
|
19
19
|
import {
|
|
20
20
|
SUPPORT_PASSPORT_DRAFT_MAX_CONTENT_CHARACTERS,
|
|
21
21
|
SUPPORT_PASSPORT_DRAFT_MAX_MEMORIES,
|
|
@@ -56,12 +56,12 @@ import {
|
|
|
56
56
|
notifySupportPassportCommitted,
|
|
57
57
|
resolveSupportPassportModelRoutePlan,
|
|
58
58
|
syncDirectoryForDurability
|
|
59
|
-
} from "../chunk-
|
|
59
|
+
} from "../chunk-ZBYVR5W4.js";
|
|
60
60
|
import "../chunk-W4F3C7N7.js";
|
|
61
61
|
import "../chunk-MPC7TYTZ.js";
|
|
62
62
|
import "../chunk-VVX5PZFS.js";
|
|
63
63
|
import "../chunk-IFBSZGW3.js";
|
|
64
|
-
import "../chunk-
|
|
64
|
+
import "../chunk-OGXXGHW6.js";
|
|
65
65
|
import "../chunk-C3QXITIP.js";
|
|
66
66
|
import "../chunk-VQ7B2Q6M.js";
|
|
67
67
|
import "../chunk-43IQRLEF.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/core",
|
|
3
|
-
"version": "9.69.
|
|
3
|
+
"version": "9.69.55",
|
|
4
4
|
"description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -3307,7 +3307,7 @@
|
|
|
3307
3307
|
"core"
|
|
3308
3308
|
],
|
|
3309
3309
|
"peerDependencies": {
|
|
3310
|
-
"@remnic/coding-graph": "^9.69.
|
|
3310
|
+
"@remnic/coding-graph": "^9.69.55"
|
|
3311
3311
|
},
|
|
3312
3312
|
"peerDependenciesMeta": {
|
|
3313
3313
|
"@remnic/coding-graph": {
|
|
@@ -137,6 +137,25 @@ test("parseActivityConfig accepts localhost and 127.x loopback hosts", () => {
|
|
|
137
137
|
);
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
test("parseActivityConfig shares the core loopback classification (issue #3012)", () => {
|
|
141
|
+
// RFC 6761 loopback subdomain and the IPv4-mapped ::1 are loopback.
|
|
142
|
+
assert.equal(
|
|
143
|
+
parseActivityConfig({ enabled: true, sources: [{ machineLabel: "a", baseUrl: "http://daemon.localhost:4319" }] })
|
|
144
|
+
.sources.length,
|
|
145
|
+
1,
|
|
146
|
+
);
|
|
147
|
+
assert.equal(
|
|
148
|
+
parseActivityConfig({ enabled: true, sources: [{ machineLabel: "b", baseUrl: "http://[::ffff:127.0.0.1]:4319" }] })
|
|
149
|
+
.sources.length,
|
|
150
|
+
1,
|
|
151
|
+
);
|
|
152
|
+
// A loopback-shaped DNS name is still not loopback.
|
|
153
|
+
assert.throws(
|
|
154
|
+
() => parseActivityConfig({ enabled: true, sources: [{ machineLabel: "c", baseUrl: "http://127.0.0.1.evil.test:4319" }] }),
|
|
155
|
+
/must target a loopback host/,
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
140
159
|
test("parseActivityConfig parses and bounds autoSyncIntervalMinutes", () => {
|
|
141
160
|
assert.equal(parseActivityConfig(undefined).autoSyncIntervalMinutes, 15);
|
|
142
161
|
assert.equal(parseActivityConfig({ autoSyncIntervalMinutes: "30" }).autoSyncIntervalMinutes, 30);
|
package/src/activity/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
import { coerceBooleanLike, coerceNumber } from "../connectors/coerce.js";
|
|
4
|
+
import { isLoopbackHost } from "../runtime/http-transport.js";
|
|
4
5
|
import { assertValidTimezone } from "./digest.js";
|
|
5
6
|
import { applyLegacyJournalHeading, normalizeJournalHeading } from "./journal-heading.js";
|
|
6
7
|
import { checkVaultJournalPrerequisites } from "./journal-vault-prereq.js";
|
|
@@ -25,7 +26,6 @@ import type { ImportanceLevel } from "../types.js";
|
|
|
25
26
|
|
|
26
27
|
const EXTRACTION_MODES: readonly ActivityExtractionMode[] = ["off", "smart"];
|
|
27
28
|
const IMPORTANCE_LEVELS: readonly ImportanceLevel[] = ["critical", "high", "normal", "low", "trivial"];
|
|
28
|
-
const LOOPBACK_HOSTS: Record<string, true> = { localhost: true, "127.0.0.1": true, "::1": true };
|
|
29
29
|
|
|
30
30
|
/** The inert default: disabled, search-only, no sources, no extraction. */
|
|
31
31
|
export function defaultActivityConfig(): ActivityConfig {
|
|
@@ -91,9 +91,8 @@ export function validateActivityBaseUrl(baseUrl: string): URL {
|
|
|
91
91
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
92
92
|
throw new RangeError("activity source baseUrl must use HTTP or HTTPS");
|
|
93
93
|
}
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
if (LOOPBACK_HOSTS[host] !== true && !/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host)) {
|
|
94
|
+
// Shared classification (brackets, 127.0.0.0/8, *.localhost, mapped v4).
|
|
95
|
+
if (!isLoopbackHost(parsed.hostname)) {
|
|
97
96
|
throw new RangeError(`activity source baseUrl must target a loopback host (got ${parsed.hostname})`);
|
|
98
97
|
}
|
|
99
98
|
return parsed;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression: #1957 governance controls 2-4 wired into the REAL manager
|
|
3
|
+
* synthesis and curation paths (issue #3012).
|
|
4
|
+
*
|
|
5
|
+
* - Control 2: an expired item never appears in cross-signals sources or the
|
|
6
|
+
* roundtable; a superseded item that still circulates is marked, not dropped.
|
|
7
|
+
* - Control 3: two agents ruling differently on the same feedback ref surface
|
|
8
|
+
* as a disputed section citing both — neither entry is silently dropped.
|
|
9
|
+
* - Control 4: the persisted report carries curator provenance and every
|
|
10
|
+
* synthesized overlap claim cites source item ids (lint tripwire holds).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
18
|
+
import { SharedContextManager } from "./manager.js";
|
|
19
|
+
import { parseConfig } from "../config.js";
|
|
20
|
+
|
|
21
|
+
async function makeManager() {
|
|
22
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-gov-wiring-"));
|
|
23
|
+
const config = parseConfig({
|
|
24
|
+
sharedContextEnabled: true,
|
|
25
|
+
sharedContextDir: dir,
|
|
26
|
+
});
|
|
27
|
+
const manager = new SharedContextManager(config);
|
|
28
|
+
await manager.ensureStructure();
|
|
29
|
+
return { manager, dir };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function today(): string {
|
|
33
|
+
return new Date().toISOString().slice(0, 10);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Stored-item shape the write path cannot produce (past expiry), but the
|
|
37
|
+
* read path must still classify: legacy or externally written items. */
|
|
38
|
+
async function writeStoredItem(
|
|
39
|
+
dir: string,
|
|
40
|
+
opts: { agent: string; title: string; expiresAt?: string; supersedes?: string },
|
|
41
|
+
): Promise<string> {
|
|
42
|
+
const dayDir = path.join(dir, "agent-outputs", opts.agent, today());
|
|
43
|
+
await mkdir(dayDir, { recursive: true });
|
|
44
|
+
const fp = path.join(dayDir, `000000-${opts.title.replace(/\s+/g, "-").toLowerCase()}.md`);
|
|
45
|
+
const frontmatter = [
|
|
46
|
+
"kind: agent_output",
|
|
47
|
+
`agent: ${JSON.stringify(opts.agent)}`,
|
|
48
|
+
`createdAt: ${new Date().toISOString()}`,
|
|
49
|
+
`title: ${JSON.stringify(opts.title)}`,
|
|
50
|
+
...(opts.expiresAt ? [`expiresAt: ${JSON.stringify(opts.expiresAt)}`] : []),
|
|
51
|
+
...(opts.supersedes ? [`supersedes: ${JSON.stringify(opts.supersedes)}`] : []),
|
|
52
|
+
];
|
|
53
|
+
await writeFile(fp, `---\n${frontmatter.join("\n")}\n---\n\nbody text about deploy runbook\n`, "utf-8");
|
|
54
|
+
return fp;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test("expired items never appear in cross-signals or curation; superseded circulation is marked", async (t) => {
|
|
58
|
+
const { manager, dir } = await makeManager();
|
|
59
|
+
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
60
|
+
|
|
61
|
+
const livePath = await writeStoredItem(dir, { agent: "agent-live", title: "live item" });
|
|
62
|
+
const expiredPath = await writeStoredItem(dir, {
|
|
63
|
+
agent: "agent-old",
|
|
64
|
+
title: "expired item",
|
|
65
|
+
expiresAt: "2020-01-01T00:00:00.000Z",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const signals = await manager.synthesizeCrossSignals({ date: today() });
|
|
69
|
+
const paths = signals.report.sources.map((source) => source.path);
|
|
70
|
+
assert.ok(paths.includes(livePath), "live item must appear in sources");
|
|
71
|
+
assert.ok(!paths.includes(expiredPath), "expired item must never appear in sources");
|
|
72
|
+
assert.equal(signals.report.staleness.expiredDropped, 1);
|
|
73
|
+
assert.deepEqual(signals.report.staleness.supersededStillCirculating, []);
|
|
74
|
+
|
|
75
|
+
const curation = await manager.curateDaily({ date: today() });
|
|
76
|
+
const roundtable = await readFile(curation.roundtablePath, "utf-8");
|
|
77
|
+
assert.ok(!roundtable.includes(expiredPath), "expired item must never appear in the roundtable");
|
|
78
|
+
assert.match(roundtable, /## Staleness/);
|
|
79
|
+
assert.match(roundtable, /Expired items dropped: 1/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("a superseded item that still circulates is marked, not dropped", async (t) => {
|
|
83
|
+
const { manager, dir } = await makeManager();
|
|
84
|
+
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
85
|
+
|
|
86
|
+
const olderPath = await writeStoredItem(dir, { agent: "agent-a", title: "older item" });
|
|
87
|
+
await writeStoredItem(dir, { agent: "agent-b", title: "newer item", supersedes: olderPath });
|
|
88
|
+
|
|
89
|
+
const signals = await manager.synthesizeCrossSignals({ date: today() });
|
|
90
|
+
assert.deepEqual(signals.report.staleness.supersededStillCirculating, [olderPath]);
|
|
91
|
+
const paths = signals.report.sources.map((source) => source.path);
|
|
92
|
+
assert.ok(paths.includes(olderPath), "superseded item still circulates (marked, not dropped)");
|
|
93
|
+
|
|
94
|
+
const curation = await manager.curateDaily({ date: today() });
|
|
95
|
+
const roundtable = await readFile(curation.roundtablePath, "utf-8");
|
|
96
|
+
assert.match(roundtable, /Superseded items still circulating: 1/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("contradictory agent rulings on the same ref surface as disputed, both preserved", async (t) => {
|
|
100
|
+
const { manager, dir } = await makeManager();
|
|
101
|
+
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
102
|
+
|
|
103
|
+
await manager.appendFeedback({
|
|
104
|
+
agent: "agent-a",
|
|
105
|
+
decision: "approved",
|
|
106
|
+
reason: "matches the runbook",
|
|
107
|
+
date: `${today()}T10:00:00.000Z`,
|
|
108
|
+
refs: ["out-0001"],
|
|
109
|
+
});
|
|
110
|
+
await manager.appendFeedback({
|
|
111
|
+
agent: "agent-b",
|
|
112
|
+
decision: "rejected",
|
|
113
|
+
reason: "misses the rollback step",
|
|
114
|
+
date: `${today()}T11:00:00.000Z`,
|
|
115
|
+
refs: ["out-0001"],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const signals = await manager.synthesizeCrossSignals({ date: today() });
|
|
119
|
+
assert.deepEqual(signals.report.disputed, [
|
|
120
|
+
{ ref: "out-0001", agents: ["agent-a", "agent-b"], decisions: ["approved", "rejected"] },
|
|
121
|
+
]);
|
|
122
|
+
assert.equal(signals.report.feedbackEntries.length, 2, "neither disputed entry is dropped");
|
|
123
|
+
|
|
124
|
+
const curation = await manager.curateDaily({ date: today() });
|
|
125
|
+
const roundtable = await readFile(curation.roundtablePath, "utf-8");
|
|
126
|
+
assert.match(roundtable, /## Disputed/);
|
|
127
|
+
assert.match(roundtable, /`out-0001` disputed by agent-a, agent-b/);
|
|
128
|
+
assert.match(roundtable, /agent-a\] approved/);
|
|
129
|
+
assert.match(roundtable, /agent-b\] rejected/);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("curation report carries provenance and cited synthesized claims", async (t) => {
|
|
133
|
+
const { manager, dir } = await makeManager();
|
|
134
|
+
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
135
|
+
|
|
136
|
+
// Two agents on one topic produce a synthesized overlap claim.
|
|
137
|
+
await manager.writeAgentOutput({ agentId: "agent-a", title: "deploy runbook notes", content: "deploy runbook steps" });
|
|
138
|
+
await manager.writeAgentOutput({ agentId: "agent-b", title: "deploy runbook review", content: "deploy runbook feedback" });
|
|
139
|
+
|
|
140
|
+
const signals = await manager.synthesizeCrossSignals({ date: today() });
|
|
141
|
+
assert.equal(signals.report.provenance.actor, "shared-context:curation");
|
|
142
|
+
assert.ok(!Number.isNaN(Date.parse(signals.report.provenance.at)), "provenance.at is a valid instant");
|
|
143
|
+
assert.ok(signals.report.overlaps.length > 0, "shared topic produces an overlap claim");
|
|
144
|
+
for (const overlap of signals.report.overlaps) {
|
|
145
|
+
assert.ok(overlap.sourcePaths.length > 0, `overlap ${overlap.token} cites source item ids`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The curation self-check gates the roundtable: it throws on an
|
|
149
|
+
// uncited synthesized claim and passes when every claim cites.
|
|
150
|
+
const curation = await manager.curateDaily({ date: today() });
|
|
151
|
+
const roundtable = await readFile(curation.roundtablePath, "utf-8");
|
|
152
|
+
assert.match(roundtable, /\[sources: [^\]]+\]/, "synthesized overlap claims cite their sources");
|
|
153
|
+
});
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
resolveWriteOrigin,
|
|
16
16
|
} from "./envelope-io.js";
|
|
17
17
|
import type { SharedAuthority, SharedEnvelope } from "./governance.js";
|
|
18
|
+
import { detectContradictionPair } from "./contradictions.js";
|
|
19
|
+
import { lintCuratedClaims } from "./curation-lint.js";
|
|
20
|
+
import { stampProvenance } from "./provenance.js";
|
|
21
|
+
import { filterLiveEnvelopes, markSupersededCirculation } from "./staleness.js";
|
|
18
22
|
|
|
19
23
|
export const SharedFeedbackEntrySchema = z.object({
|
|
20
24
|
agent: z.string().min(1),
|
|
@@ -341,6 +345,15 @@ interface SharedCrossSignalReport {
|
|
|
341
345
|
feedbackEntries: SharedFeedbackEntry[];
|
|
342
346
|
sources: SharedCrossSignalSource[];
|
|
343
347
|
overlaps: SharedCrossSignalOverlap[];
|
|
348
|
+
/** Control 2 (issue #1957): expiry drops and supersession circulation. */
|
|
349
|
+
staleness: {
|
|
350
|
+
expiredDropped: number;
|
|
351
|
+
supersededStillCirculating: string[];
|
|
352
|
+
};
|
|
353
|
+
/** Control 3 (issue #1957): refs two agents ruled on differently. */
|
|
354
|
+
disputed: Array<{ ref: string; agents: string[]; decisions: string[] }>;
|
|
355
|
+
/** Control 4 (issue #1957): who synthesized this report and when. */
|
|
356
|
+
provenance: { actor: string; at: string };
|
|
344
357
|
semantic: {
|
|
345
358
|
enabled: boolean;
|
|
346
359
|
applied: boolean;
|
|
@@ -418,6 +431,23 @@ function formatOverlapLine(entry: SharedCrossSignalOverlap): string {
|
|
|
418
431
|
return `- \`${entry.token}\` (${entry.agentCount} agents: ${markdownLineList(entry.agents)}) [sources: ${markdownLineList(entry.sourcePaths)}]`;
|
|
419
432
|
}
|
|
420
433
|
|
|
434
|
+
function formatStalenessLines(report: SharedCrossSignalReport): string[] {
|
|
435
|
+
const circulating = report.staleness.supersededStillCirculating;
|
|
436
|
+
return [
|
|
437
|
+
`- Expired items dropped: ${report.staleness.expiredDropped}`,
|
|
438
|
+
circulating.length === 0
|
|
439
|
+
? "- Superseded items still circulating: 0"
|
|
440
|
+
: `- Superseded items still circulating: ${circulating.length} (${markdownLineList(circulating)})`,
|
|
441
|
+
];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function formatDisputedLines(report: SharedCrossSignalReport): string[] {
|
|
445
|
+
if (report.disputed.length === 0) return ["- No contradictory agent judgments recorded."];
|
|
446
|
+
return report.disputed.map((entry) =>
|
|
447
|
+
`- \`${entry.ref}\` disputed by ${markdownLineList(entry.agents)} (decisions: ${markdownLineList(entry.decisions)})`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
421
451
|
async function writeFileAtomic(filePath: string, content: string): Promise<void> {
|
|
422
452
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
423
453
|
const tempPath = path.join(
|
|
@@ -730,6 +760,20 @@ export class SharedContextManager {
|
|
|
730
760
|
|| a.title.localeCompare(b.title),
|
|
731
761
|
);
|
|
732
762
|
|
|
763
|
+
// Control 2 (issue #1957): expired items never enter the working set;
|
|
764
|
+
// items another live item supersedes are marked while they circulate.
|
|
765
|
+
const liveOutputs = filterLiveEnvelopes(
|
|
766
|
+
outputs.map((output) => ({ output, expiresAt: output.envelope.expiresAt })),
|
|
767
|
+
Date.now(),
|
|
768
|
+
).map((entry) => entry.output);
|
|
769
|
+
const expiredDropped = outputs.length - liveOutputs.length;
|
|
770
|
+
const supersededStillCirculating = markSupersededCirculation(
|
|
771
|
+
liveOutputs.map((output) => ({ id: output.path, supersedes: output.envelope.supersedes })),
|
|
772
|
+
)
|
|
773
|
+
.filter((item) => item.circulating === true)
|
|
774
|
+
.map((item) => item.id)
|
|
775
|
+
.sort();
|
|
776
|
+
|
|
733
777
|
// Collect feedback entries for the day.
|
|
734
778
|
const feedback: SharedFeedbackEntry[] = [];
|
|
735
779
|
try {
|
|
@@ -749,7 +793,7 @@ export class SharedContextManager {
|
|
|
749
793
|
// ignore
|
|
750
794
|
}
|
|
751
795
|
|
|
752
|
-
const sources: SharedCrossSignalSource[] =
|
|
796
|
+
const sources: SharedCrossSignalSource[] = liveOutputs.map((output) => {
|
|
753
797
|
const body = stripYamlFrontmatter(output.raw);
|
|
754
798
|
return {
|
|
755
799
|
agent: output.agent,
|
|
@@ -828,15 +872,44 @@ export class SharedContextManager {
|
|
|
828
872
|
feedbackByDecision[entry.decision] += 1;
|
|
829
873
|
}
|
|
830
874
|
|
|
831
|
-
|
|
875
|
+
// Control 3 (issue #1957): feedback refs are claim keys; two agents
|
|
876
|
+
// ruling differently on the same ref surface as disputed, never silently
|
|
877
|
+
// averaged. Pairwise scan is bounded by one day's feedback entries.
|
|
878
|
+
const claimItems = feedback.map((entry, index) => ({
|
|
879
|
+
id: `${entry.agent}#${index}`,
|
|
880
|
+
claims: Object.fromEntries((entry.refs ?? []).map((ref) => [ref, entry.decision])),
|
|
881
|
+
}));
|
|
882
|
+
const disputedByRef = new Map<string, { agents: Set<string>; decisions: Set<string> }>();
|
|
883
|
+
for (let i = 0; i < claimItems.length; i++) {
|
|
884
|
+
for (let j = i + 1; j < claimItems.length; j++) {
|
|
885
|
+
const pair = detectContradictionPair(claimItems[i]!, claimItems[j]!);
|
|
886
|
+
if (pair.kind !== "conflict") continue;
|
|
887
|
+
for (const ref of pair.keys) {
|
|
888
|
+
const slot = disputedByRef.get(ref) ?? { agents: new Set<string>(), decisions: new Set<string>() };
|
|
889
|
+
slot.agents.add(feedback[i]!.agent);
|
|
890
|
+
slot.agents.add(feedback[j]!.agent);
|
|
891
|
+
slot.decisions.add(feedback[i]!.decision);
|
|
892
|
+
slot.decisions.add(feedback[j]!.decision);
|
|
893
|
+
disputedByRef.set(ref, slot);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
const disputed = [...disputedByRef.entries()]
|
|
898
|
+
.map(([ref, slot]) => ({ ref, agents: [...slot.agents].sort(), decisions: [...slot.decisions].sort() }))
|
|
899
|
+
.sort((a, b) => a.ref.localeCompare(b.ref));
|
|
900
|
+
|
|
901
|
+
const generatedAt = new Date().toISOString();
|
|
902
|
+
const baseReport = {
|
|
832
903
|
date,
|
|
833
|
-
generatedAt
|
|
904
|
+
generatedAt,
|
|
834
905
|
sourceCount: sources.length,
|
|
835
906
|
feedbackCount: feedback.length,
|
|
836
907
|
feedbackByDecision,
|
|
837
908
|
feedbackEntries: [...feedback].sort(compareFeedbackPriority),
|
|
838
909
|
sources,
|
|
839
910
|
overlaps: mergedOverlaps,
|
|
911
|
+
staleness: { expiredDropped, supersededStillCirculating },
|
|
912
|
+
disputed,
|
|
840
913
|
semantic: {
|
|
841
914
|
enabled: semanticEnabled,
|
|
842
915
|
applied: semanticApplied,
|
|
@@ -846,6 +919,8 @@ export class SharedContextManager {
|
|
|
846
919
|
addedOverlapCount: semanticAddedOverlapCount,
|
|
847
920
|
},
|
|
848
921
|
};
|
|
922
|
+
// Control 4 (issue #1957): stamp who synthesized the report and when.
|
|
923
|
+
const report = stampProvenance(baseReport, { actor: "shared-context:curation", at: generatedAt });
|
|
849
924
|
|
|
850
925
|
const crossSignalsPath = path.join(this.crossSignalsDir, `${date}.json`);
|
|
851
926
|
await writeFileAtomic(crossSignalsPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
@@ -879,6 +954,12 @@ export class SharedContextManager {
|
|
|
879
954
|
`- Semantic enhancer: ${semanticEnabled ? (semanticTimedOut ? "enabled (timed out, fail-open)" : semanticApplied ? "enabled (applied)" : "enabled (no additional overlaps)") : "disabled"}`,
|
|
880
955
|
`- JSON report: ${crossSignalsPath}`,
|
|
881
956
|
"",
|
|
957
|
+
"## Staleness",
|
|
958
|
+
...formatStalenessLines(report),
|
|
959
|
+
"",
|
|
960
|
+
"## Disputed",
|
|
961
|
+
...formatDisputedLines(report),
|
|
962
|
+
"",
|
|
882
963
|
"## Recurring Themes",
|
|
883
964
|
...recurringThemeLines,
|
|
884
965
|
"",
|
|
@@ -916,6 +997,19 @@ export class SharedContextManager {
|
|
|
916
997
|
const date = opts.date;
|
|
917
998
|
const maxChars = Math.max(2_000, opts.maxChars ?? 20_000);
|
|
918
999
|
const crossSignals = await this.synthesizeCrossSignalsUnlocked({ date });
|
|
1000
|
+
// Control 4 (issue #1957): every synthesized claim must cite at least one
|
|
1001
|
+
// item this run actually had. Overlaps are built from sources, so this is
|
|
1002
|
+
// a tripwire — it fires only if a synthesized section stops citing.
|
|
1003
|
+
const lintFindings = lintCuratedClaims({
|
|
1004
|
+
claims: crossSignals.report.overlaps.map((overlap) => ({
|
|
1005
|
+
claimId: `overlap:${overlap.token}`,
|
|
1006
|
+
citedItemIds: overlap.sourcePaths,
|
|
1007
|
+
})),
|
|
1008
|
+
availableItemIds: crossSignals.report.sources.map((source) => source.path),
|
|
1009
|
+
});
|
|
1010
|
+
if (lintFindings.length > 0) {
|
|
1011
|
+
throw new Error(`shared-context curation self-check failed: ${JSON.stringify(lintFindings)}`);
|
|
1012
|
+
}
|
|
919
1013
|
const feedbackLines = crossSignals.report.feedbackEntries.length === 0
|
|
920
1014
|
? ["- (none)"]
|
|
921
1015
|
: crossSignals.report.feedbackEntries.map((entry) => formatFeedbackLine(entry));
|
|
@@ -944,8 +1038,15 @@ export class SharedContextManager {
|
|
|
944
1038
|
`- Cross-signals markdown: ${crossSignals.crossSignalsMarkdownPath}`,
|
|
945
1039
|
...overlapBullets,
|
|
946
1040
|
"",
|
|
1041
|
+
"## Staleness",
|
|
1042
|
+
...formatStalenessLines(crossSignals.report),
|
|
1043
|
+
"",
|
|
1044
|
+
"## Disputed",
|
|
1045
|
+
...formatDisputedLines(crossSignals.report),
|
|
1046
|
+
"",
|
|
947
1047
|
];
|
|
948
1048
|
|
|
1049
|
+
|
|
949
1050
|
const out = md.join("\n");
|
|
950
1051
|
const trimmed = out.length > maxChars ? out.slice(0, maxChars) + "\n\n...(trimmed)\n" : out;
|
|
951
1052
|
|
|
@@ -27,7 +27,7 @@ export function filterLiveEnvelopes<T extends { expiresAt?: string }>(
|
|
|
27
27
|
*/
|
|
28
28
|
export function markSupersededCirculation<T extends SharedStalenessItem>(
|
|
29
29
|
items: readonly T[],
|
|
30
|
-
): T[] {
|
|
30
|
+
): (T & { circulating?: boolean })[] {
|
|
31
31
|
const supersededIds = new Set<string>();
|
|
32
32
|
for (const item of items) {
|
|
33
33
|
if (item.supersedes) supersededIds.add(item.supersedes);
|