Compare commits
2 Commits
agent/java
...
agent/unkn
| Author | SHA1 | Date |
|---|---|---|
|
|
8245dbcb7a | |
|
|
9b0cb95786 |
|
|
@ -31,8 +31,8 @@ lint:dead` rejects unused files, exports, and dependencies.
|
|||
Set `REA_LOG_LEVEL` to `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or
|
||||
`silent` to control structured JSON diagnostics. MCP mode defaults to `info` and
|
||||
always writes logs to stderr so the stdio protocol remains intact. One-shot CLI
|
||||
logging is opt-in and writes to stdout when a level is configured, preserving
|
||||
machine-readable command output by default. Request arguments, bridge
|
||||
logging is opt-in and writes to stderr when a level is configured, preserving
|
||||
machine-readable command output. Request arguments, bridge
|
||||
authentication tokens, and environment data are redacted.
|
||||
|
||||
Changes that claim real Hopper behavior must also be tested against two distinct binaries:
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -431,6 +431,19 @@ Run `npx -y rea-agents --help` for direct decompilation, bounded search and
|
|||
other options. `analyze` and `inspect` share the same overview workflow;
|
||||
`function`, `xrefs`, and `trace` return the same Evidence v2 envelopes as MCP.
|
||||
|
||||
CLI exit status is independent of output format, full output, filtering, and
|
||||
token limits:
|
||||
|
||||
| Status | Meaning |
|
||||
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `0` | The requested operation completed truthfully. Explicit unknown, bounded, or truncated evidence remains successful when the operation completed under its declared contract. |
|
||||
| `1` | Argument validation, approval or policy, provider or analysis, integrity, cancellation or timeout, serialization, or output failed. Structured output retains the typed category and safe diagnostic details when available. |
|
||||
|
||||
Machine-readable failures remain on stdout in the requested format; incidental
|
||||
diagnostics and progress use stderr. Signal termination retains the conventional
|
||||
status supplied by the shell and runtime. Shell pipelines should enable
|
||||
`pipefail` when the REA status must propagate through later commands.
|
||||
|
||||
Or install the `rea` command globally:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
import {
|
||||
createResidualUnknown,
|
||||
updateResidualUnknown,
|
||||
residualUnknownSchema,
|
||||
type RecordUnknownInput,
|
||||
type ResidualUnknown,
|
||||
type UnknownStatus,
|
||||
type UpdateUnknownInput,
|
||||
} from "../domain/residualUnknown.js";
|
||||
import { UnknownRegistryError } from "../domain/errors.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
export interface UnknownRegistryLimits {
|
||||
readonly maxRecords: number;
|
||||
readonly maxRelationships: number;
|
||||
}
|
||||
|
||||
type UnknownRegistryMutation = Result<ResidualUnknown, UnknownRegistryError>;
|
||||
|
||||
type UnknownRegistryQuery = Result<
|
||||
readonly ResidualUnknown[],
|
||||
UnknownRegistryError
|
||||
>;
|
||||
|
||||
type UnknownRegistryVerify = Result<
|
||||
{
|
||||
readonly valid: boolean;
|
||||
readonly truthVerified: boolean;
|
||||
readonly unknown: ResidualUnknown;
|
||||
},
|
||||
UnknownRegistryError
|
||||
>;
|
||||
|
||||
/**
|
||||
* Persistent, bounded registry of residual unknowns linked to evidence.
|
||||
*
|
||||
* Each unknown carries a stable ID derived from its question and scope,
|
||||
* an immutable revision graph, and evidence-qualified resolution state.
|
||||
*/
|
||||
export class UnknownRegistry {
|
||||
readonly #revisions = new Map<string, ResidualUnknown>();
|
||||
readonly #heads = new Map<string, ResidualUnknown>();
|
||||
|
||||
constructor(private readonly limits: UnknownRegistryLimits) {
|
||||
if (!Number.isSafeInteger(limits.maxRecords) || limits.maxRecords < 1)
|
||||
throw new RangeError("maxRecords must be a positive safe integer");
|
||||
if (
|
||||
!Number.isSafeInteger(limits.maxRelationships) ||
|
||||
limits.maxRelationships < 0
|
||||
)
|
||||
throw new RangeError(
|
||||
"maxRelationships must be a non-negative safe integer",
|
||||
);
|
||||
}
|
||||
|
||||
recordUnknown(
|
||||
input: RecordUnknownInput,
|
||||
mutationEvidenceId: string,
|
||||
): UnknownRegistryMutation {
|
||||
let unknown: ResidualUnknown;
|
||||
try {
|
||||
unknown = createResidualUnknown(input, mutationEvidenceId, null);
|
||||
} catch (cause: unknown) {
|
||||
return err(new UnknownRegistryError("invalid-transition", { cause }));
|
||||
}
|
||||
if (this.#heads.has(unknown.unknown_id))
|
||||
return err(new UnknownRegistryError("already-exists"));
|
||||
return this.#append(unknown);
|
||||
}
|
||||
|
||||
updateUnknown(
|
||||
input: UpdateUnknownInput,
|
||||
mutationEvidenceId: string,
|
||||
): UnknownRegistryMutation {
|
||||
const current = this.#heads.get(input.unknown_id);
|
||||
if (current === undefined)
|
||||
return err(new UnknownRegistryError("not-found"));
|
||||
if (current.revision !== input.expected_revision)
|
||||
return err(new UnknownRegistryError("revision-conflict"));
|
||||
let updated: ResidualUnknown;
|
||||
try {
|
||||
updated = updateResidualUnknown(current, input, mutationEvidenceId);
|
||||
} catch (cause: unknown) {
|
||||
return err(new UnknownRegistryError("invalid-transition", { cause }));
|
||||
}
|
||||
return this.#append(updated);
|
||||
}
|
||||
|
||||
listUnknowns(
|
||||
filters: {
|
||||
readonly status?: UnknownStatus;
|
||||
readonly severity?: ResidualUnknown["severity"];
|
||||
readonly domain?: string;
|
||||
} = {},
|
||||
): UnknownRegistryQuery {
|
||||
return ok(
|
||||
[...this.#heads.values()]
|
||||
.filter(
|
||||
(unknown) =>
|
||||
(filters.status === undefined ||
|
||||
unknown.status === filters.status) &&
|
||||
(filters.severity === undefined ||
|
||||
unknown.severity === filters.severity) &&
|
||||
(filters.domain === undefined || unknown.domain === filters.domain),
|
||||
)
|
||||
.sort((left, right) => left.unknown_id.localeCompare(right.unknown_id)),
|
||||
);
|
||||
}
|
||||
|
||||
verifyUnknownResolution(unknownId: string): UnknownRegistryVerify {
|
||||
const unknown = this.#heads.get(unknownId);
|
||||
if (unknown === undefined)
|
||||
return err(new UnknownRegistryError("not-found"));
|
||||
return ok({
|
||||
valid: unknown.status === "resolved",
|
||||
truthVerified: unknown.resolution?.disposition === "verified",
|
||||
unknown,
|
||||
});
|
||||
}
|
||||
|
||||
get(unknownId: string): ResidualUnknown | undefined {
|
||||
const unknown = this.#heads.get(unknownId);
|
||||
return unknown === undefined ? undefined : structuredClone(unknown);
|
||||
}
|
||||
|
||||
has(unknownId: string): boolean {
|
||||
return this.#heads.has(unknownId);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.#revisions.clear();
|
||||
this.#heads.clear();
|
||||
}
|
||||
|
||||
#append(unknown: ResidualUnknown): UnknownRegistryMutation {
|
||||
const pending = this.#revisions.size + 1;
|
||||
if (pending > this.limits.maxRecords)
|
||||
return err(new UnknownRegistryError("limit"));
|
||||
if (unknown.relationships.length > this.limits.maxRelationships)
|
||||
return err(new UnknownRegistryError("limit"));
|
||||
try {
|
||||
residualUnknownSchema.parse(unknown);
|
||||
} catch (cause: unknown) {
|
||||
return err(new UnknownRegistryError("integrity", { cause }));
|
||||
}
|
||||
this.#revisions.set(
|
||||
`${unknown.unknown_id}:${String(unknown.revision)}`,
|
||||
unknown,
|
||||
);
|
||||
this.#heads.set(unknown.unknown_id, unknown);
|
||||
return ok(unknown);
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,8 @@ import { importReferenceSource } from "./application/ReferenceSourceImport.js";
|
|||
import { registerEvidenceCommands } from "./cliEvidenceCommands.js";
|
||||
import { registerProcessCommands } from "./cliProcessCommands.js";
|
||||
import { registerInvestigationCommands } from "./cliInvestigationCommands.js";
|
||||
import { registerUnknownCommands } from "./cliUnknownCommands.js";
|
||||
import { registerBrowserCommands } from "./cliBrowserCommands.js";
|
||||
import {
|
||||
AnalysisProtocolError,
|
||||
PermissionRequiredError,
|
||||
|
|
@ -27,7 +29,7 @@ import {
|
|||
import { projectReferenceSourceImportError } from "./application/ReferenceSourceImportTypes.js";
|
||||
import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
|
||||
import { registerPolicyCommands } from "./cliPolicyCommands.js";
|
||||
import { registerBrowserCommands } from "./cliBrowserCommands.js";
|
||||
import { UnknownRegistry } from "./application/UnknownRegistry.js";
|
||||
|
||||
/**
|
||||
* Build the one-shot Incur CLI without starting Hopper at import time.
|
||||
|
|
@ -71,6 +73,11 @@ export const createCli = (): ReturnType<typeof Cli.create> => {
|
|||
registerArtifactCommands(cli, logger);
|
||||
registerInvestigationCommands(cli, logger);
|
||||
registerEvidenceCommands(cli, logger);
|
||||
registerUnknownCommands(
|
||||
cli,
|
||||
logger,
|
||||
new UnknownRegistry({ maxRecords: 1_000, maxRelationships: 100 }),
|
||||
);
|
||||
registerReferenceSourceCommand(cli, logger);
|
||||
registerProcessCommands(cli, logger);
|
||||
registerPolicyCommands(cli, logger);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
import { Cli, z } from "incur";
|
||||
|
||||
import type { UnknownRegistry } from "./application/UnknownRegistry.js";
|
||||
import type { Logger } from "./logger.js";
|
||||
import type {
|
||||
RecordUnknownInput,
|
||||
UpdateUnknownInput,
|
||||
} from "./domain/residualUnknown.js";
|
||||
|
||||
/** Register residual-unknown registry CLI commands. */
|
||||
export const registerUnknownCommands = (
|
||||
cli: ReturnType<typeof Cli.create>,
|
||||
_logger: Logger,
|
||||
registry: UnknownRegistry,
|
||||
): void => {
|
||||
cli.command("unknowns", {
|
||||
description: "List recorded residual unknowns with optional filters",
|
||||
options: z.object({
|
||||
status: z
|
||||
.enum(["open", "investigating", "blocked", "contradicted", "resolved"])
|
||||
.optional()
|
||||
.describe("Filter by status"),
|
||||
severity: z
|
||||
.enum(["low", "medium", "high", "critical"])
|
||||
.optional()
|
||||
.describe("Filter by severity"),
|
||||
domain: z.string().optional().describe("Filter by domain"),
|
||||
}),
|
||||
run: ({ options }): ReturnType<typeof registry.listUnknowns> => {
|
||||
const filters: {
|
||||
status?:
|
||||
| "open"
|
||||
| "investigating"
|
||||
| "blocked"
|
||||
| "contradicted"
|
||||
| "resolved";
|
||||
severity?: "low" | "medium" | "high" | "critical";
|
||||
domain?: string;
|
||||
} = {};
|
||||
if (options.status !== undefined) filters.status = options.status;
|
||||
if (options.severity !== undefined) filters.severity = options.severity;
|
||||
if (options.domain !== undefined) filters.domain = options.domain;
|
||||
return registry.listUnknowns(filters);
|
||||
},
|
||||
});
|
||||
|
||||
cli.command("unknown-record", {
|
||||
description: "Record one approved residual unknown question",
|
||||
options: z.object({
|
||||
question: z.string().min(1).max(1_000).describe("The bounded question"),
|
||||
severity: z
|
||||
.enum(["low", "medium", "high", "critical"])
|
||||
.describe("Severity of the unknown"),
|
||||
domain: z.string().min(1).max(100).describe("Domain of the unknown"),
|
||||
authority: z
|
||||
.enum([
|
||||
"shipped-artifact",
|
||||
"controlled-replay",
|
||||
"historical-reference",
|
||||
"external-service",
|
||||
"analyst-inference",
|
||||
])
|
||||
.describe("Required authority to resolve"),
|
||||
confidence: z
|
||||
.enum(["observed", "derived", "inferred"])
|
||||
.describe("Required confidence level"),
|
||||
evidence: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe("Mutation evidence ID for this record operation"),
|
||||
}),
|
||||
run: ({ options }) =>
|
||||
registry.recordUnknown(
|
||||
{
|
||||
question: options.question,
|
||||
severity: options.severity,
|
||||
domain: options.domain,
|
||||
required_authority: options.authority,
|
||||
required_confidence: options.confidence,
|
||||
required_environment: null,
|
||||
supporting_evidence_ids: [],
|
||||
contradicting_evidence_ids: [],
|
||||
recommended_probes: [],
|
||||
relationships: [],
|
||||
approved: true,
|
||||
} as RecordUnknownInput,
|
||||
options.evidence,
|
||||
),
|
||||
});
|
||||
|
||||
cli.command("unknown-update", {
|
||||
description: "Update one residual unknown with new evidence or status",
|
||||
options: z.object({
|
||||
id: z.string().describe("Unknown ID (unk_…)"),
|
||||
revision: z.number().int().min(1).describe("Expected current revision"),
|
||||
status: z
|
||||
.enum(["open", "investigating", "blocked", "contradicted", "resolved"])
|
||||
.describe("New status"),
|
||||
severity: z
|
||||
.enum(["low", "medium", "high", "critical"])
|
||||
.describe("New severity"),
|
||||
evidence: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe("Mutation evidence ID for this update operation"),
|
||||
}),
|
||||
run: ({ options }) =>
|
||||
registry.updateUnknown(
|
||||
{
|
||||
unknown_id: options.id,
|
||||
expected_revision: options.revision,
|
||||
approved: true,
|
||||
status: options.status,
|
||||
severity: options.severity,
|
||||
required_authority: null,
|
||||
required_confidence: "observed",
|
||||
required_environment: null,
|
||||
supporting_evidence_ids: [],
|
||||
contradicting_evidence_ids: [],
|
||||
recommended_probes: [],
|
||||
relationships: [],
|
||||
resolution: null,
|
||||
} as unknown as UpdateUnknownInput,
|
||||
options.evidence,
|
||||
),
|
||||
});
|
||||
|
||||
cli.command("unknown-verify", {
|
||||
description: "Verify the resolution state of one residual unknown",
|
||||
options: z.object({
|
||||
id: z.string().describe("Unknown ID (unk_…)"),
|
||||
}),
|
||||
run: ({ options }) => registry.verifyUnknownResolution(options.id),
|
||||
});
|
||||
};
|
||||
|
|
@ -12,7 +12,7 @@ const logLevelSchema = z
|
|||
export const parseLogLevel = (value: unknown): LogLevel =>
|
||||
logLevelSchema.parse(value);
|
||||
|
||||
/** Create a JSON logger on stderr so protocol and CLI result stdout stay valid. */
|
||||
/** Create a JSON logger whose stderr destination cannot corrupt structured output. */
|
||||
export const createLogger = (mode: "mcp" | "cli", level: LogLevel): Logger =>
|
||||
pino(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,226 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { UnknownRegistry } from "../src/application/UnknownRegistry.js";
|
||||
|
||||
const limits = { maxRecords: 100, maxRelationships: 50 };
|
||||
|
||||
const baseRecordInput = {
|
||||
question: "What triggers the notification delegate?",
|
||||
severity: "high" as const,
|
||||
domain: "notifications",
|
||||
required_authority: "shipped-artifact" as const,
|
||||
required_confidence: "observed" as const,
|
||||
required_environment: null,
|
||||
supporting_evidence_ids: [] as string[],
|
||||
contradicting_evidence_ids: [] as string[],
|
||||
recommended_probes: [],
|
||||
relationships: [],
|
||||
approved: true as const,
|
||||
};
|
||||
|
||||
describe("UnknownRegistry", () => {
|
||||
it("creates a registry entry with deterministic ID and revision 1", () => {
|
||||
const registry = new UnknownRegistry(limits);
|
||||
const result = registry.recordUnknown(
|
||||
baseRecordInput,
|
||||
"ev_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
|
||||
);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
const unk = result.value;
|
||||
expect(unk.unknown_id).toMatch(/^unk_[a-f0-9]{64}$/u);
|
||||
expect(unk.revision).toBe(1);
|
||||
expect(unk.status).toBe("open");
|
||||
expect(unk.previous_revision_digest).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects duplicate creation with already-exists", () => {
|
||||
const registry = new UnknownRegistry(limits);
|
||||
const first = registry.recordUnknown(
|
||||
baseRecordInput,
|
||||
"ev_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
|
||||
);
|
||||
expect(first.ok).toBe(true);
|
||||
const second = registry.recordUnknown(
|
||||
baseRecordInput,
|
||||
"ev_b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
|
||||
);
|
||||
expect(second.ok).toBe(false);
|
||||
if (second.ok) return;
|
||||
expect(second.error.reason).toBe("already-exists");
|
||||
});
|
||||
|
||||
it("updates status and bumps revision with optimistic concurrency", () => {
|
||||
const registry = new UnknownRegistry(limits);
|
||||
const created = registry.recordUnknown(
|
||||
baseRecordInput,
|
||||
"ev_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
|
||||
);
|
||||
if (!created.ok) throw new Error("create failed");
|
||||
const updated = registry.updateUnknown(
|
||||
{
|
||||
unknown_id: created.value.unknown_id,
|
||||
expected_revision: 1,
|
||||
approved: true,
|
||||
status: "investigating",
|
||||
severity: "high",
|
||||
required_authority: null,
|
||||
required_confidence: "observed",
|
||||
required_environment: null,
|
||||
supporting_evidence_ids: [],
|
||||
contradicting_evidence_ids: [],
|
||||
recommended_probes: [],
|
||||
relationships: [],
|
||||
resolution: null,
|
||||
},
|
||||
"ev_b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
|
||||
);
|
||||
expect(updated.ok).toBe(true);
|
||||
if (!updated.ok) return;
|
||||
expect(updated.value.revision).toBe(2);
|
||||
expect(updated.value.status).toBe("investigating");
|
||||
expect(updated.value.previous_revision_digest).toBe(
|
||||
created.value.revision_digest,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects update with wrong expected revision", () => {
|
||||
const registry = new UnknownRegistry(limits);
|
||||
const created = registry.recordUnknown(
|
||||
baseRecordInput,
|
||||
"ev_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
|
||||
);
|
||||
if (!created.ok) throw new Error("create failed");
|
||||
const updated = registry.updateUnknown(
|
||||
{
|
||||
unknown_id: created.value.unknown_id,
|
||||
expected_revision: 99,
|
||||
approved: true,
|
||||
status: "investigating",
|
||||
severity: "high",
|
||||
required_authority: null,
|
||||
required_confidence: "observed",
|
||||
required_environment: null,
|
||||
supporting_evidence_ids: [],
|
||||
contradicting_evidence_ids: [],
|
||||
recommended_probes: [],
|
||||
relationships: [],
|
||||
resolution: null,
|
||||
},
|
||||
"ev_b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
|
||||
);
|
||||
expect(updated.ok).toBe(false);
|
||||
if (updated.ok) return;
|
||||
expect(updated.error.reason).toBe("revision-conflict");
|
||||
});
|
||||
|
||||
it("rejects update of non-existent unknown", () => {
|
||||
const registry = new UnknownRegistry(limits);
|
||||
const updated = registry.updateUnknown(
|
||||
{
|
||||
unknown_id: "unk_" + "0".repeat(64),
|
||||
expected_revision: 1,
|
||||
approved: true,
|
||||
status: "investigating",
|
||||
severity: "high",
|
||||
required_authority: null,
|
||||
required_confidence: "observed",
|
||||
required_environment: null,
|
||||
supporting_evidence_ids: [],
|
||||
contradicting_evidence_ids: [],
|
||||
recommended_probes: [],
|
||||
relationships: [],
|
||||
resolution: null,
|
||||
},
|
||||
"ev_b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
|
||||
);
|
||||
expect(updated.ok).toBe(false);
|
||||
if (updated.ok) return;
|
||||
expect(updated.error.reason).toBe("not-found");
|
||||
});
|
||||
|
||||
it("lists unknowns filtered by status and domain", () => {
|
||||
const registry = new UnknownRegistry(limits);
|
||||
const a = registry.recordUnknown(
|
||||
{ ...baseRecordInput, domain: "notifications" },
|
||||
"ev_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
|
||||
);
|
||||
const b = registry.recordUnknown(
|
||||
{ ...baseRecordInput, question: "How does IPC work?", domain: "ipc" },
|
||||
"ev_b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
|
||||
);
|
||||
expect(a.ok).toBe(true);
|
||||
expect(b.ok).toBe(true);
|
||||
|
||||
const allOpen = registry.listUnknowns({ status: "open" });
|
||||
expect(allOpen.ok).toBe(true);
|
||||
if (!allOpen.ok) return;
|
||||
expect(allOpen.value).toHaveLength(2);
|
||||
|
||||
const ipcOnly = registry.listUnknowns({ domain: "ipc" });
|
||||
expect(ipcOnly.ok).toBe(true);
|
||||
if (!ipcOnly.ok) return;
|
||||
expect(ipcOnly.value).toHaveLength(1);
|
||||
expect(ipcOnly.value[0]?.domain).toBe("ipc");
|
||||
});
|
||||
|
||||
it("verifies resolution requires verified disposition", () => {
|
||||
const registry = new UnknownRegistry(limits);
|
||||
const created = registry.recordUnknown(
|
||||
baseRecordInput,
|
||||
"ev_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
|
||||
);
|
||||
if (!created.ok) throw new Error("create failed");
|
||||
|
||||
// Cannot resolve without verified evidence
|
||||
const unresolved = registry.updateUnknown(
|
||||
{
|
||||
unknown_id: created.value.unknown_id,
|
||||
expected_revision: 1,
|
||||
approved: true,
|
||||
status: "resolved",
|
||||
severity: "high",
|
||||
required_authority: null,
|
||||
required_confidence: "observed",
|
||||
required_environment: null,
|
||||
supporting_evidence_ids: [],
|
||||
contradicting_evidence_ids: [],
|
||||
recommended_probes: [],
|
||||
relationships: [],
|
||||
resolution: {
|
||||
disposition: "verified",
|
||||
rationale: "Test resolved",
|
||||
evidence_ids: [],
|
||||
},
|
||||
},
|
||||
"ev_b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
|
||||
);
|
||||
// The domain layer should reject resolving with verified disposition
|
||||
// and no evidence ids
|
||||
expect(unresolved.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("enforces record limits", () => {
|
||||
const smallLimits = { maxRecords: 2, maxRelationships: 50 };
|
||||
const registry = new UnknownRegistry(smallLimits);
|
||||
expect(
|
||||
registry.recordUnknown(
|
||||
baseRecordInput,
|
||||
"ev_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
|
||||
).ok,
|
||||
).toBe(true);
|
||||
expect(
|
||||
registry.recordUnknown(
|
||||
{ ...baseRecordInput, question: "Second?" },
|
||||
"ev_b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
|
||||
).ok,
|
||||
).toBe(true);
|
||||
const third = registry.recordUnknown(
|
||||
{ ...baseRecordInput, question: "Third?" },
|
||||
"ev_c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3",
|
||||
);
|
||||
expect(third.ok).toBe(false);
|
||||
if (third.ok) return;
|
||||
expect(third.error.reason).toBe("limit");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue