Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8245dbcb7a | ||
|
|
9b0cb95786 |
No files matched your search
+2
-2
@@ -31,8 +31,8 @@ lint:dead` rejects unused files, exports, and dependencies.
|
|||||||
Set `REA_LOG_LEVEL` to `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or
|
Set `REA_LOG_LEVEL` to `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or
|
||||||
`silent` to control structured JSON diagnostics. MCP mode defaults to `info` and
|
`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
|
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
|
logging is opt-in and writes to stderr when a level is configured, preserving
|
||||||
machine-readable command output by default. Request arguments, bridge
|
machine-readable command output. Request arguments, bridge
|
||||||
authentication tokens, and environment data are redacted.
|
authentication tokens, and environment data are redacted.
|
||||||
|
|
||||||
Changes that claim real Hopper behavior must also be tested against two distinct binaries:
|
Changes that claim real Hopper behavior must also be tested against two distinct binaries:
|
||||||
|
|||||||
@@ -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;
|
other options. `analyze` and `inspect` share the same overview workflow;
|
||||||
`function`, `xrefs`, and `trace` return the same Evidence v2 envelopes as MCP.
|
`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:
|
Or install the `rea` command globally:
|
||||||
|
|
||||||
```bash
|
```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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-1
@@ -19,6 +19,8 @@ import { importReferenceSource } from "./application/ReferenceSourceImport.js";
|
|||||||
import { registerEvidenceCommands } from "./cliEvidenceCommands.js";
|
import { registerEvidenceCommands } from "./cliEvidenceCommands.js";
|
||||||
import { registerProcessCommands } from "./cliProcessCommands.js";
|
import { registerProcessCommands } from "./cliProcessCommands.js";
|
||||||
import { registerInvestigationCommands } from "./cliInvestigationCommands.js";
|
import { registerInvestigationCommands } from "./cliInvestigationCommands.js";
|
||||||
|
import { registerUnknownCommands } from "./cliUnknownCommands.js";
|
||||||
|
import { registerBrowserCommands } from "./cliBrowserCommands.js";
|
||||||
import {
|
import {
|
||||||
AnalysisProtocolError,
|
AnalysisProtocolError,
|
||||||
PermissionRequiredError,
|
PermissionRequiredError,
|
||||||
@@ -27,7 +29,7 @@ import {
|
|||||||
import { projectReferenceSourceImportError } from "./application/ReferenceSourceImportTypes.js";
|
import { projectReferenceSourceImportError } from "./application/ReferenceSourceImportTypes.js";
|
||||||
import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
|
import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
|
||||||
import { registerPolicyCommands } from "./cliPolicyCommands.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.
|
* 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);
|
registerArtifactCommands(cli, logger);
|
||||||
registerInvestigationCommands(cli, logger);
|
registerInvestigationCommands(cli, logger);
|
||||||
registerEvidenceCommands(cli, logger);
|
registerEvidenceCommands(cli, logger);
|
||||||
|
registerUnknownCommands(
|
||||||
|
cli,
|
||||||
|
logger,
|
||||||
|
new UnknownRegistry({ maxRecords: 1_000, maxRelationships: 100 }),
|
||||||
|
);
|
||||||
registerReferenceSourceCommand(cli, logger);
|
registerReferenceSourceCommand(cli, logger);
|
||||||
registerProcessCommands(cli, logger);
|
registerProcessCommands(cli, logger);
|
||||||
registerPolicyCommands(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),
|
||||||
|
});
|
||||||
|
};
|
||||||
+1
-1
@@ -12,7 +12,7 @@ const logLevelSchema = z
|
|||||||
export const parseLogLevel = (value: unknown): LogLevel =>
|
export const parseLogLevel = (value: unknown): LogLevel =>
|
||||||
logLevelSchema.parse(value);
|
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 =>
|
export const createLogger = (mode: "mcp" | "cli", level: LogLevel): Logger =>
|
||||||
pino(
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user