mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
refactor: return complete analysis results inline
This commit is contained in:
@@ -218,7 +218,7 @@ declaring type + normalized CLI signature
|
||||
build-local metadata token
|
||||
RVA/file extent when present
|
||||
raw CIL byte length + SHA-256 when the body is admitted
|
||||
member-result schema version + decoded-instruction-tuple SHA-256 when CIL is valid
|
||||
decoded-instruction-tuple SHA-256 when CIL is valid
|
||||
separate max-stack, locals-token, and exception-region observations
|
||||
```
|
||||
|
||||
@@ -232,9 +232,8 @@ The caller-visible field remains named `normalized_il_sha256`, but its shipped
|
||||
v1 meaning is deliberately narrower than a canonical semantic CIL identity.
|
||||
For a completely decoded method, REA serializes the instruction-order array of
|
||||
`[opcode, operand_kind, operand]` tuples with JavaScript `JSON.stringify`,
|
||||
encodes that text as UTF-8, and reports its lowercase SHA-256. The member
|
||||
result's `schema_version: 1` scopes this convention; there is no independent
|
||||
normalized-CIL algorithm-version field in v1.
|
||||
encodes that text as UTF-8, and reports its lowercase SHA-256. The tuple
|
||||
projection is defined here; the member result carries no schema-version field.
|
||||
|
||||
Operands retain the current decoder projection: metadata and user-string
|
||||
operands are build-local lowercase hexadecimal tokens, scalar branch operands
|
||||
@@ -258,8 +257,8 @@ normalized-CIL schema.
|
||||
|
||||
Metadata tokens are reported because they are precise coordinates inside one
|
||||
module. Cross-version identity never consists of a token alone. A structural
|
||||
match cites both build commitments, both local tokens, the v1 tuple inputs, the
|
||||
member-result schema version, and all competing candidates. V1 cannot cite a
|
||||
match cites both build commitments, both local tokens, the v1 tuple inputs, and
|
||||
all competing candidates. V1 cannot cite a
|
||||
separate matching-algorithm version because none is exposed.
|
||||
|
||||
### 5. Separate five evidence layers
|
||||
|
||||
@@ -121,8 +121,8 @@ boundaries:
|
||||
|
||||
The normalized field is available only when the entire CIL stream decodes with
|
||||
status `present`. A partial, malformed, absent, or over-limit body reports
|
||||
`null`. The containing member result's `schema_version: 1` scopes the tuple
|
||||
format; v1 does not expose a separate algorithm-version field.
|
||||
`null`. The tuple format is defined here and does not add a schema-version field
|
||||
to the member result.
|
||||
|
||||
Tuple operands use these exact projections:
|
||||
|
||||
@@ -220,7 +220,7 @@ Cross-version matching is two-stage:
|
||||
1. Exact identity requires compatible artifact/module commitments and exact
|
||||
raw CIL identity; v1 does not expose a standalone complete-method-body
|
||||
digest.
|
||||
2. Structural identity compares schema-versioned signatures, the limited v1
|
||||
2. Structural identity compares normalized signatures, the limited v1
|
||||
decoded-CIL tuple fingerprint, constants, and bounded shape context. It
|
||||
reports all candidates at the winning score and remains ambiguous when the
|
||||
evidence does not distinguish them. The v1 digest does not itself remap
|
||||
@@ -327,7 +327,6 @@ absolute or relative to the manifest file. A compact manifest contains:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"label": "operator-local osu!stable semantic slice",
|
||||
"target": {
|
||||
"path": "./osu!.exe",
|
||||
|
||||
@@ -39,9 +39,7 @@ rea build-reconstruction-obligation-ledger \
|
||||
"manifest": {
|
||||
"bindings": [],
|
||||
"contradictions": []
|
||||
},
|
||||
"limits": { "max_obligations": 100 },
|
||||
"page": { "offset": 0, "limit": 50 }
|
||||
}
|
||||
}' \
|
||||
--json
|
||||
```
|
||||
@@ -322,8 +320,6 @@ close a `packaged-process` obligation. A static-only source cannot close a
|
||||
runtime or process claim. Marking a claim `blocked` or `out-of-scope` records the
|
||||
disposition but does not silently count it as verified.
|
||||
|
||||
Pagination changes only `page` and the returned `obligations` slice. Every page
|
||||
retains the same `ledger_id`, `closure_digest`, full-ledger summaries, reports,
|
||||
ownership graph, dependency graph, Evidence links, and limitations. Consumers
|
||||
should follow `next_offset` until it is `null`, and should reject pages whose
|
||||
identity or closure digest changes mid-read.
|
||||
The tool returns every obligation inline. Its `ledger_id` and `closure_digest`
|
||||
identify the complete result, including summaries, reports, ownership and
|
||||
dependency graphs, Evidence links, and limitations.
|
||||
|
||||
@@ -1,103 +1,102 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"ledger_id": "ecl_2d5c1c83c1fd56615a663bcb3a432b7023afa316133b744c64dcaa659cab53c0",
|
||||
"ledger_id": "ecl_578e5853b332ae050e69dc1a2c72fc98be527371311d6a98a9ae3bf28a24fa6c",
|
||||
"records": [
|
||||
{
|
||||
"claim_id": "managed.application-graph-projection",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_0c73f33f51048a7c7cc5d908cb951512ee9f3d9dcf4124f0f0eb2c4a88ceb7cc"
|
||||
"ev_a09c9014515baea9a709c1d5c0ae32d343a1f1b6e4da498ce5afe14eaab0ddbb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.decompiler-reconstruction-import",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_0504a7fc4cc8f77ca07a3aa59d62830b5769c7b41e7518677771fe7aa54c7b5d"
|
||||
"ev_14052dcba244a91019c042feae78440d800570dde163c991931d9c09b7e6bbdd"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.dotnet-framework-x86-pinvoke",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_9b63cdeef9d48e567dc8f4de9fe6b424e94929ac1f17aafd0ef1807775f72d96"
|
||||
"ev_c11aa40e3fbc2de8bb11d6b5783ee3a74d4d79fe16ea7f6ddd85d4b253c6d769"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.ilspy-reconstruction-oracle",
|
||||
"status": "unsupported",
|
||||
"evidence_ids": [
|
||||
"ev_4e3fbc7203c29aab5d9119332a09c14e285e85007b3d7fc7433880b39a507435"
|
||||
"ev_76ca183c167d63e6c58b65e2f9b3bef3e569669dc66abb0b8b850948878a70e3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.malformed-metadata",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_dbd885adc65f9cc8abacb04642db609d2180b0af71adc0b0732719f06a057063"
|
||||
"ev_99e1185da1fcbf1330b4b86beca27e0abeef10f95870b1982da39827278e16e8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.managed-native-verification",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_e7841bf63d7b83804987d21f4f432752870ff7338862360bbcf54653eb782731"
|
||||
"ev_9d1c5bc641c0da1551c8fbae4507d7719847cfb40edf73c13e2bdf5fb670ec9e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.modern-dotnet-anycpu",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_9593b026617002c64d0618ec079d42dbc0ddeef7cd53f2f8b2c8fca9ffebb2a9"
|
||||
"ev_7f959cb9642a2c52da0cce0d3c9c888b889240ee7697c5fdb5390d7578634782"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.mvid-and-token-drift",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_ba32694a35810acd211745efdf41c88887f4931efb6027e876eea3e8e506aee0"
|
||||
"ev_dd6c3e3fbe115f1b9b38ee06f720d5efb7dd6ef693a1b9555e9ff73babdaea9f"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.not-managed",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_ac700cf6c8efdda7f689b29a9c3d5a679ecb24893f93bb1c76e50e23b2b9dcbb"
|
||||
"ev_ca183a8f4159136df29d9d5e44a88239c996d6e758b71ad023289871875a9367"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.operator-local-manifest",
|
||||
"status": "unsupported",
|
||||
"evidence_ids": [
|
||||
"ev_f96f07bc28f2e6da5a71597f9a38f5eaa7cde0217f0df10d6ef6d655937fc1a2"
|
||||
"ev_4173365e113f71b28fd1f3f1c583557a487e36ec6a6d0e63b1249eb7a4334b8c"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.operator-manifest-self-test",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_ed0f3ff1749f0927d493609423a366ec6a432f016c767aeeedd9b4a520672eaf"
|
||||
"ev_063c68b649379811e56c7788ab6b01891f1dd78abfe3008c6bf06a1c0727a095"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.runtime-correlation-admission-plan",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_24a622e16a885e2d21025f51c3f4b5900719c21d9f0d04ba1d55f6093a702c20"
|
||||
"ev_530ff114b2f05f85b6baeaa56df60a60a927f1de3b07bbbe86aa462faad71772"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.unicode-obfuscated-identifiers",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_2e9bc5213917635c3eb3d094449adf088c450c07d078dcb26312182c04966bc9"
|
||||
"ev_17f9013d5ce058be847c1f3efec19688c268fa2feecf974bbc18428cd87144e5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.x64-ready-to-run-native-body",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_bbf3dd8daec6d46d16ac7befc3875572e7103b27b815f81903e3dca61880c70b"
|
||||
"ev_60dc353ad66b83ee50163ab55fb0f4c7caefd06e08ccc2e0ebb5d2a7a23cb55b"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"manifest_id": "ecm_3a42d32a96315338e06700b64afc0367603e2c5050b1ec0bf1ce012e227b4df3",
|
||||
"manifest_id": "ecm_0f98c38501954b90ff9f949f1ee9a0b550061347cf92780cfb1b14ccef5131f7",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
@@ -14,7 +13,7 @@
|
||||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "89b09782340415790ecf161a5bc33f60e1a23cd66e0f4c29dd26e959b2baa0a9"
|
||||
"sha256": "b78dbf9a5f0db15415f03dccab71bfb2d133900ab061e37825f55f3b7f0f80bc"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
@@ -30,8 +29,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.decompiler-reconstruction-import",
|
||||
@@ -45,8 +43,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.dotnet-framework-x86-pinvoke",
|
||||
@@ -60,8 +57,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.ilspy-reconstruction-oracle",
|
||||
@@ -73,8 +69,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.malformed-metadata",
|
||||
@@ -88,8 +83,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.managed-native-verification",
|
||||
@@ -104,8 +98,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.modern-dotnet-anycpu",
|
||||
@@ -119,8 +112,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.mvid-and-token-drift",
|
||||
@@ -135,8 +127,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.not-managed",
|
||||
@@ -150,8 +141,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.operator-local-manifest",
|
||||
@@ -163,8 +153,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.operator-manifest-self-test",
|
||||
@@ -178,8 +167,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.runtime-correlation-admission-plan",
|
||||
@@ -193,8 +181,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.unicode-obfuscated-identifiers",
|
||||
@@ -208,8 +195,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.x64-ready-to-run-native-body",
|
||||
@@ -223,8 +209,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -95,8 +95,8 @@ and no verified no-follow equivalent for every path component.
|
||||
|
||||
Linux continues to use a mode-restricted Unix socket. Windows uses a Java
|
||||
listener bound to `127.0.0.1` on an ephemeral port. The bridge atomically
|
||||
publishes a bounded endpoint record containing only schema version, literal
|
||||
loopback host, and port. The random 256-bit request token remains only in the
|
||||
publishes a bounded endpoint record containing only the literal loopback host
|
||||
and port. The random 256-bit request token remains only in the
|
||||
ephemeral session descriptor and every request is authenticated.
|
||||
|
||||
`analyzeHeadless.bat` is invoked through an absolute `cmd.exe` with a fixed
|
||||
|
||||
@@ -291,7 +291,6 @@ def diagnostic(
|
||||
(0, False, "") if capture is None else capture.snapshot()
|
||||
)
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"component": "hopper_private_display",
|
||||
"operation": operation,
|
||||
"status": status,
|
||||
|
||||
@@ -42,7 +42,6 @@ const duplicateOriginStorage = (origin) => ({
|
||||
/** Build the source-owned full-capture browser scenario. */
|
||||
export function browserScenario(browser, origin) {
|
||||
return browserScenarioSchema.parse({
|
||||
schema_version: 1,
|
||||
browser,
|
||||
start_url: { url: `${origin}/app`, query: [] },
|
||||
allowed_origins: [origin],
|
||||
|
||||
@@ -22,7 +22,6 @@ const completionClaim = ({
|
||||
parameters: {
|
||||
scenario_id: scenarioId,
|
||||
scenario_version: 1,
|
||||
result_schema_version: 1,
|
||||
artifact_sha256s: artifactSha256s,
|
||||
},
|
||||
result: { status },
|
||||
@@ -37,7 +36,6 @@ const completionClaim = ({
|
||||
id: COMPLETION_PROVIDER.id,
|
||||
version: COMPLETION_PROVIDER.version,
|
||||
},
|
||||
result_schema_version: 1,
|
||||
status,
|
||||
evidence_ids: [evidence.evidence_id],
|
||||
};
|
||||
@@ -151,7 +149,6 @@ const optionalClaims = ({ operatorManifest, ilspyOracle }) => [
|
||||
|
||||
/** Project completed managed assertions into the standard verifier report. */
|
||||
export const createManagedCompletionReport = (input, verifierRun) => ({
|
||||
schema_version: 1,
|
||||
verifier: {
|
||||
id: COMPLETION_PROVIDER.id,
|
||||
version: COMPLETION_PROVIDER.version,
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
export function parseManagedAppManifest(rawManifest) {
|
||||
const manifest = object(rawManifest, "manifest");
|
||||
ensure(
|
||||
manifest.schema_version === 1,
|
||||
"manifest.schema_version must be exactly 1",
|
||||
);
|
||||
const target = object(manifest.target, "manifest.target");
|
||||
const methods = array(manifest.methods, "manifest.methods");
|
||||
ensure(manifest.methods.length > 0, "manifest.methods must not be empty");
|
||||
return {
|
||||
schema_version: 1,
|
||||
label: optionalString(manifest.label, "manifest.label"),
|
||||
target: {
|
||||
path: string(target.path, "manifest.target.path"),
|
||||
|
||||
@@ -43,7 +43,6 @@ async function runManagedAppManifestSelfTest(context) {
|
||||
assert.ok(method);
|
||||
const summary = await context.verifyManagedAppManifest(
|
||||
{
|
||||
schema_version: 1,
|
||||
label: "source-owned manifest verifier self-test",
|
||||
target: {
|
||||
path: sample.path,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export interface VerifierRunStart {
|
||||
readonly schema_version: 1;
|
||||
readonly run_id: string;
|
||||
readonly verifier_pid: number;
|
||||
readonly parent_pid: number;
|
||||
@@ -14,7 +13,6 @@ export interface VerifierRunDescendant {
|
||||
export type VerifierProcessLineage =
|
||||
| {
|
||||
readonly status: "verified";
|
||||
readonly schema_version: 1;
|
||||
readonly observed_at: string;
|
||||
readonly launcher_pid: number;
|
||||
readonly launcher_parent_pid: number;
|
||||
|
||||
@@ -11,7 +11,6 @@ export const createVerifierRun = () => {
|
||||
return activeVerifierRun;
|
||||
}
|
||||
activeVerifierRun = Object.freeze({
|
||||
schema_version: 1,
|
||||
run_id: randomUUID(),
|
||||
verifier_pid: process.pid,
|
||||
parent_pid: process.ppid,
|
||||
@@ -85,7 +84,6 @@ const observeVerifierLineage = async (run, observedAt) => {
|
||||
}
|
||||
return {
|
||||
status: "verified",
|
||||
schema_version: 1,
|
||||
observed_at: observedAt,
|
||||
launcher_pid: run.verifier_pid,
|
||||
launcher_parent_pid: run.parent_pid,
|
||||
|
||||
@@ -5,7 +5,6 @@ const REQUIRED_HELP_COMMANDS = [
|
||||
"upgrade",
|
||||
"inventory-artifact",
|
||||
"extract-artifact",
|
||||
"investigate-versions",
|
||||
"import-reference-source",
|
||||
"list-browser-targets",
|
||||
"inspect-web-page",
|
||||
|
||||
@@ -20,7 +20,6 @@ const applicationRoot =
|
||||
const applicationPath =
|
||||
process.env.REA_ELECTRON_APPLICATION_PATH ?? join(applicationRoot, "main.js");
|
||||
const input = electronActiveObservationInputSchema.parse({
|
||||
schema_version: 1,
|
||||
executable_path: executable,
|
||||
application_path: applicationPath,
|
||||
application_root: applicationRoot,
|
||||
|
||||
@@ -71,7 +71,6 @@ try {
|
||||
throw new Error("Real Node Inspector capture was incomplete");
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
schema_version: 1,
|
||||
status: "pass",
|
||||
provider: observed.provider.id,
|
||||
target_id: target.target_id,
|
||||
|
||||
@@ -157,7 +157,6 @@ try {
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
schema_version: 1,
|
||||
status: cliReport.status,
|
||||
report_id: cliReport.report_id,
|
||||
report_digest: cliReport.report_digest,
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, an
|
||||
metadata:
|
||||
version: "23"
|
||||
tool_count: 115
|
||||
catalog_digest: "94f7cd1ac629a6bbffa77ba3b3d3f2e208e15df821bc0b14db4f81692d6f8eaa"
|
||||
catalog_digest: "85d3e5545b3e01eb22b67a1794fdcb737654a1abbea803e1f2445815ee4f17ff"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
||||
@@ -396,8 +396,6 @@ const capability = (provider: ProviderIdentity): CapabilityDescriptor => ({
|
||||
operation: "address_name",
|
||||
available: true,
|
||||
reason: null,
|
||||
pagination: "none",
|
||||
exhaustive: true,
|
||||
effects: {
|
||||
mutatesArtifact: false,
|
||||
launchesProcess: true,
|
||||
@@ -407,7 +405,6 @@ const capability = (provider: ProviderIdentity): CapabilityDescriptor => ({
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
},
|
||||
limits: { maxResults: null, maxPayloadBytes: 1_000, timeoutMs: 1_000 },
|
||||
limitations: [],
|
||||
});
|
||||
|
||||
|
||||
@@ -161,7 +161,6 @@ export const resolveCompareApplicationVersionsRequestValidated = (
|
||||
...input.right_native_observations,
|
||||
...rightNative.value,
|
||||
],
|
||||
limits: input.limits,
|
||||
...(input.unknown_registry_approved === undefined
|
||||
? {}
|
||||
: { unknown_registry_approved: input.unknown_registry_approved }),
|
||||
@@ -241,7 +240,6 @@ export const resolveCompareJavaScriptExportShapesRequestValidated = (
|
||||
left_export_name: input.left_export_name,
|
||||
right_module_path: input.right_module_path,
|
||||
right_export_name: input.right_export_name,
|
||||
limits: input.limits,
|
||||
...(input.unknown_registry_approved === undefined
|
||||
? {}
|
||||
: { unknown_registry_approved: input.unknown_registry_approved }),
|
||||
|
||||
@@ -53,8 +53,6 @@ export const exportEvidenceBundleCommand = async (
|
||||
export const compareEvidenceBundlesCommand = async (input: {
|
||||
readonly leftPath: string;
|
||||
readonly rightPath: string;
|
||||
readonly offset: number;
|
||||
readonly limit: number;
|
||||
readonly policy: EvidenceFilePolicy;
|
||||
}): Promise<Result<JsonValue, AnalysisError>> => {
|
||||
const [left, right] = await Promise.all([
|
||||
@@ -64,11 +62,7 @@ export const compareEvidenceBundlesCommand = async (input: {
|
||||
if (!left.ok) return left;
|
||||
if (!right.ok) return right;
|
||||
try {
|
||||
return ok(
|
||||
jsonValueSchema.parse(
|
||||
compareBundles(left.value, right.value, [], input.offset, input.limit),
|
||||
),
|
||||
);
|
||||
return ok(jsonValueSchema.parse(compareBundles(left.value, right.value)));
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
new EvidenceIntegrityError("Evidence bundle comparison failed", {
|
||||
|
||||
@@ -119,9 +119,9 @@ export const compareApplicationVersionsEvidenceValidated = (
|
||||
rootArtifactSha256: right.rootArtifactSha256,
|
||||
graph: right.graph,
|
||||
},
|
||||
limits: input.limits,
|
||||
leftNativeEvidence: leftNative,
|
||||
rightNativeEvidence: rightNative,
|
||||
limits: input.limits,
|
||||
});
|
||||
return ok(
|
||||
createApplicationVersionComparisonEvidence(
|
||||
@@ -214,7 +214,6 @@ export const compareJavaScriptExportShapesEvidenceValidated = (
|
||||
modulePath: input.right_module_path,
|
||||
exportName: input.right_export_name,
|
||||
},
|
||||
limits: input.limits,
|
||||
});
|
||||
return ok(
|
||||
createJavaScriptExportShapeComparisonEvidence(
|
||||
@@ -225,7 +224,6 @@ export const compareJavaScriptExportShapesEvidenceValidated = (
|
||||
left_export_name: input.left_export_name,
|
||||
right_module_path: input.right_module_path,
|
||||
right_export_name: input.right_export_name,
|
||||
limits: input.limits,
|
||||
},
|
||||
result,
|
||||
),
|
||||
|
||||
@@ -125,7 +125,6 @@ export const prepareReplayPlan = async (
|
||||
: prepareSide(input.right, input.limits.module_bytes, host),
|
||||
]);
|
||||
const withoutDigest = {
|
||||
schema_version: 1 as const,
|
||||
policy_version: REPLAY_POLICY_VERSION,
|
||||
policy_sha256: digestJson(REPLAY_POLICY_DOCUMENT),
|
||||
network: "none" as const,
|
||||
|
||||
@@ -310,7 +310,6 @@ const applyReproducerExport = async (
|
||||
return executed;
|
||||
|
||||
const manifest = {
|
||||
schema_version: 1,
|
||||
plan: prepared.publicPlan,
|
||||
result: executed,
|
||||
sources: exportContext.includeSources
|
||||
|
||||
@@ -54,7 +54,7 @@ it("projects closure and direct interprocedural flow without execution", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps dynamic calls explicit and returns bounded deterministic pages", () => {
|
||||
it("keeps dynamic calls explicit and returns complete deterministic results", () => {
|
||||
const graph = graphFor(`
|
||||
const handlers = { ready() { return 1; } };
|
||||
const key = process.argv[2];
|
||||
@@ -73,21 +73,11 @@ it("keeps dynamic calls explicit and returns bounded deterministic pages", () =>
|
||||
seed: { kind: "semantic-node", node_id: seedNodeId },
|
||||
direction: "forward-influence",
|
||||
include_ambiguous_dynamic_edges: true,
|
||||
limits: {
|
||||
max_seed_matches: 25,
|
||||
max_nodes: 2_000,
|
||||
max_edges: 4_000,
|
||||
max_depth: 12,
|
||||
max_functions: 500,
|
||||
max_modules: 500,
|
||||
page_size: 1,
|
||||
},
|
||||
});
|
||||
const repeated = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "semantic-node", node_id: seedNodeId },
|
||||
direction: "forward-influence",
|
||||
include_ambiguous_dynamic_edges: true,
|
||||
limits: first.applied_limits,
|
||||
});
|
||||
expect(repeated).toEqual(first);
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { queryJavaScriptSemanticGraph } from "../domain/javascriptSemanticQuery.js";
|
||||
import { JavaScriptSemanticQueryCursorError } from "../domain/javascriptSemanticQueryIdentity.js";
|
||||
import {
|
||||
javaScriptSemanticTraceResultSchema,
|
||||
traceJavaScriptSemanticsInputSchema,
|
||||
@@ -55,16 +54,6 @@ export const traceJavaScriptSemanticsEvidenceValidated = (
|
||||
),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof JavaScriptSemanticQueryCursorError)
|
||||
return err(
|
||||
new AnalysisInputError(OPERATION, undefined, [
|
||||
{
|
||||
path: ["query", "cursor"],
|
||||
reason: "invalid_value",
|
||||
expected: "a cursor returned by the same graph, query, and limits",
|
||||
},
|
||||
]),
|
||||
);
|
||||
return err(
|
||||
new AnalysisProtocolError("JavaScript semantic trace failed", { cause }),
|
||||
);
|
||||
|
||||
@@ -284,7 +284,6 @@ const createCharacterizationPlan = (
|
||||
replayPlan: ReplayPlan,
|
||||
): RuntimeCharacterizationPlan =>
|
||||
createRuntimeCharacterizationPlan({
|
||||
schema_version: 1,
|
||||
preparation_sha256: digestPreparation(
|
||||
instrumentation.manifest.instrumented_sha256,
|
||||
replayPlan.plan_digest,
|
||||
|
||||
@@ -11,9 +11,9 @@ import { createEvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { processCaptureSchema } from "../domain/processCapture.js";
|
||||
import {
|
||||
reconstructionObligationLedgerPageSchema,
|
||||
reconstructionObligationLedgerSchema,
|
||||
type ReconstructionObligationLedgerInput,
|
||||
type ReconstructionObligationLedgerPage,
|
||||
type ReconstructionObligationLedger,
|
||||
type ReviewedReconstructionObligation,
|
||||
} from "../domain/reconstructionObligationLedgerSchemas.js";
|
||||
|
||||
@@ -81,26 +81,25 @@ const request = (
|
||||
evidence_bundle: createEvidenceBundle(records),
|
||||
reviewed_obligations: [],
|
||||
manifest: { bindings: [], contradictions: [] },
|
||||
page: { offset: 0, limit: 50 },
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const build = (
|
||||
input: ReconstructionObligationLedgerInput,
|
||||
): ReconstructionObligationLedgerPage => {
|
||||
): ReconstructionObligationLedger => {
|
||||
const parsed = resolveReconstructionObligationLedgerRequest(input);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
const result = buildReconstructionObligationLedgerEvidenceValidated(
|
||||
parsed.value,
|
||||
);
|
||||
if (!result.ok) throw result.error;
|
||||
return reconstructionObligationLedgerPageSchema.parse(
|
||||
return reconstructionObligationLedgerSchema.parse(
|
||||
result.value.normalized_result,
|
||||
);
|
||||
};
|
||||
|
||||
const originalCases = (
|
||||
obligation: ReconstructionObligationLedgerPage["obligations"][number],
|
||||
obligation: ReconstructionObligationLedger["obligations"][number],
|
||||
evidenceId: string,
|
||||
) =>
|
||||
obligation.required_case_kinds.map((caseKind) => ({
|
||||
|
||||
@@ -11,10 +11,10 @@ import { createEvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { processCaptureSchema } from "../domain/processCapture.js";
|
||||
import {
|
||||
reconstructionObligationLedgerPageSchema,
|
||||
reconstructionObligationLedgerSchema,
|
||||
serializeReconstructionObligationLedger,
|
||||
type ReconstructionObligationLedgerInput,
|
||||
type ReconstructionObligationLedgerPage,
|
||||
type ReconstructionObligationLedger,
|
||||
type ReviewedReconstructionObligation,
|
||||
} from "../domain/reconstructionObligationLedgerSchemas.js";
|
||||
|
||||
@@ -82,36 +82,25 @@ const request = (
|
||||
evidence_bundle: createEvidenceBundle(records),
|
||||
reviewed_obligations: [],
|
||||
manifest: { bindings: [], contradictions: [] },
|
||||
page: { offset: 0, limit: 50 },
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const build = (
|
||||
input: ReconstructionObligationLedgerInput,
|
||||
): ReconstructionObligationLedgerPage => {
|
||||
): ReconstructionObligationLedger => {
|
||||
const parsed = resolveReconstructionObligationLedgerRequest(input);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
const result = buildReconstructionObligationLedgerEvidenceValidated(
|
||||
parsed.value,
|
||||
);
|
||||
if (!result.ok) throw result.error;
|
||||
return reconstructionObligationLedgerPageSchema.parse(
|
||||
return reconstructionObligationLedgerSchema.parse(
|
||||
result.value.normalized_result,
|
||||
);
|
||||
};
|
||||
|
||||
const completeLedger = (
|
||||
page: ReconstructionObligationLedgerPage,
|
||||
obligations: ReconstructionObligationLedgerPage["obligations"],
|
||||
) => {
|
||||
const { page: pagination, ...ledger } = page;
|
||||
if (pagination.total !== obligations.length)
|
||||
throw new TypeError("Incomplete obligation page set");
|
||||
return { ...ledger, obligations };
|
||||
};
|
||||
|
||||
const originalCases = (
|
||||
obligation: ReconstructionObligationLedgerPage["obligations"][number],
|
||||
obligation: ReconstructionObligationLedger["obligations"][number],
|
||||
evidenceId: string,
|
||||
) =>
|
||||
obligation.required_case_kinds.map((caseKind) => ({
|
||||
@@ -150,45 +139,26 @@ const reviewedObligation = (
|
||||
});
|
||||
|
||||
describe("reconstruction obligation ledger", () => {
|
||||
it("emits deterministic closure identity and paging", () => {
|
||||
it("emits the complete deterministic ledger and closure identity", () => {
|
||||
const evidence = proofEvidence("review");
|
||||
const reviewed = [
|
||||
reviewedObligation("obl.review.one", evidence.evidence_id),
|
||||
reviewedObligation("obl.review.two", evidence.evidence_id),
|
||||
];
|
||||
const first = build(
|
||||
request([evidence], {
|
||||
reviewed_obligations: reviewed,
|
||||
page: { offset: 0, limit: 1 },
|
||||
}),
|
||||
request([evidence], { reviewed_obligations: reviewed }),
|
||||
);
|
||||
const second = build(
|
||||
request([evidence], {
|
||||
reviewed_obligations: reviewed,
|
||||
page: { offset: 1, limit: 1 },
|
||||
}),
|
||||
request([evidence], { reviewed_obligations: reviewed }),
|
||||
);
|
||||
|
||||
expect(first.page).toEqual({
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
total: 2,
|
||||
returned: 1,
|
||||
next_offset: 1,
|
||||
});
|
||||
expect(second.page.next_offset).toBeNull();
|
||||
expect(first.obligations).toHaveLength(2);
|
||||
expect(first).toEqual(second);
|
||||
expect(second.ledger_id).toBe(first.ledger_id);
|
||||
expect(second.closure_digest).toBe(first.closure_digest);
|
||||
expect(first.obligations[0]?.source_state).toBe("reviewed");
|
||||
expect(first.status).toBe("open");
|
||||
expect(
|
||||
serializeReconstructionObligationLedger(
|
||||
completeLedger(first, [...first.obligations, ...second.obligations]),
|
||||
),
|
||||
).toBe(
|
||||
serializeReconstructionObligationLedger(
|
||||
completeLedger(second, [...first.obligations, ...second.obligations]),
|
||||
),
|
||||
expect(serializeReconstructionObligationLedger(first)).toBe(
|
||||
serializeReconstructionObligationLedger(second),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { jsonObjectSchema, jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import {
|
||||
reconstructionObligationLedgerInputSchema,
|
||||
reconstructionObligationLedgerPageSchema,
|
||||
reconstructionObligationLedgerSchema,
|
||||
type ReconstructionObligationLedgerInput,
|
||||
type ReconstructionObligationLedgerPage,
|
||||
type ReconstructionObligationLedger,
|
||||
} from "../domain/reconstructionObligationLedgerSchemas.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER } from "./InvestigationProviders.js";
|
||||
@@ -30,7 +30,7 @@ export const resolveReconstructionObligationLedgerRequest = (
|
||||
: err(new AnalysisInputError(OPERATION, { cause: parsed.error }));
|
||||
};
|
||||
|
||||
/** Build one deterministic ledger page and wrap it in portable Evidence. */
|
||||
/** Build the complete deterministic ledger and wrap it in portable Evidence. */
|
||||
export const buildReconstructionObligationLedgerEvidenceValidated = (
|
||||
input: ReconstructionObligationLedgerInput,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
@@ -45,8 +45,7 @@ export const buildReconstructionObligationLedgerEvidenceValidated = (
|
||||
manifest: input.manifest,
|
||||
generationLimitations: generated.limitations,
|
||||
});
|
||||
const page = pageLedger(ledger, input.page.offset, input.page.limit);
|
||||
return ok(createLedgerEvidence(input, page));
|
||||
return ok(createLedgerEvidence(input, ledger));
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof z.ZodError)
|
||||
return err(new AnalysisInputError(OPERATION, { cause }));
|
||||
@@ -59,47 +58,24 @@ export const buildReconstructionObligationLedgerEvidenceValidated = (
|
||||
}
|
||||
};
|
||||
|
||||
const pageLedger = (
|
||||
ledger: ReturnType<typeof evaluateReconstructionObligationLedger>,
|
||||
offset: number,
|
||||
limit: number,
|
||||
): ReconstructionObligationLedgerPage => {
|
||||
const obligations = ledger.obligations.slice(offset, offset + limit);
|
||||
const nextOffset =
|
||||
offset + obligations.length < ledger.obligations.length
|
||||
? offset + obligations.length
|
||||
: null;
|
||||
const { obligations: _allObligations, ...summary } = ledger;
|
||||
return reconstructionObligationLedgerPageSchema.parse({
|
||||
...summary,
|
||||
page: {
|
||||
offset,
|
||||
limit,
|
||||
total: ledger.obligations.length,
|
||||
returned: obligations.length,
|
||||
next_offset: nextOffset,
|
||||
},
|
||||
obligations,
|
||||
});
|
||||
};
|
||||
|
||||
const createLedgerEvidence = (
|
||||
input: ReconstructionObligationLedgerInput,
|
||||
page: ReconstructionObligationLedgerPage,
|
||||
): Evidence =>
|
||||
createEvidence(undefined, JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER, {
|
||||
ledger: ReconstructionObligationLedger,
|
||||
): Evidence => {
|
||||
const completeLedger = reconstructionObligationLedgerSchema.parse(ledger);
|
||||
return createEvidence(undefined, JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER, {
|
||||
predicateType: "rea.reconstruction-obligation-ledger/v1",
|
||||
operation: OPERATION,
|
||||
parameters: jsonObjectSchema.parse({
|
||||
ledger_id: page.ledger_id,
|
||||
closure_digest: page.closure_digest,
|
||||
page: input.page,
|
||||
ledger_id: completeLedger.ledger_id,
|
||||
closure_digest: completeLedger.closure_digest,
|
||||
}),
|
||||
result: jsonValueSchema.parse(page),
|
||||
result: jsonValueSchema.parse(completeLedger),
|
||||
rawResult: null,
|
||||
confidence: "inferred",
|
||||
authority: "analyst-inference",
|
||||
environment: null,
|
||||
limitations: page.limitations,
|
||||
evidenceLinks: page.evidence_links,
|
||||
limitations: completeLedger.limitations,
|
||||
evidenceLinks: completeLedger.evidence_links,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -125,8 +125,8 @@ const buildPermissionCeilings = (
|
||||
const ceilings: PermissionCeiling[] = [
|
||||
permissionScope("evidence_read", arrays.evidenceRoots),
|
||||
permissionScope("evidence_write", arrays.evidenceRoots),
|
||||
permissionScope("investigation_workspace_read", arrays.evidenceRoots),
|
||||
permissionScope("investigation_workspace_write", arrays.evidenceRoots),
|
||||
permissionScope("reconstruction_coverage_read", arrays.evidenceRoots),
|
||||
permissionScope("reconstruction_coverage_write", arrays.evidenceRoots),
|
||||
permissionScope("snapshot_read", arrays.analysisSnapshotRoots),
|
||||
permissionScope("snapshot_write", arrays.analysisSnapshotRoots),
|
||||
permissionScope("artifact_extract", ["/"]),
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { reconstructionClosureResultSchema } from "../domain/reconstructionCoverage.js";
|
||||
import {
|
||||
reconstructionObligationLedgerInputSchema,
|
||||
reconstructionObligationLedgerPageSchema,
|
||||
reconstructionObligationLedgerSchema,
|
||||
} from "../domain/reconstructionObligationLedgerSchemas.js";
|
||||
import {
|
||||
reconstructionReadinessInputSchema,
|
||||
@@ -60,7 +60,7 @@ const exportShapeComparisonOutputSchema = evidenceResultOf(
|
||||
javaScriptExportShapeComparisonResultSchema,
|
||||
);
|
||||
const reconstructionObligationLedgerOutputSchema = evidenceResultOf(
|
||||
reconstructionObligationLedgerPageSchema,
|
||||
reconstructionObligationLedgerSchema,
|
||||
);
|
||||
const reconstructionReadinessOutputSchema = evidenceResultOf(
|
||||
reconstructionReadinessReportSchema,
|
||||
@@ -301,13 +301,13 @@ export const APPLICATION_TOOL_CONTRACTS = [
|
||||
name: "build_reconstruction_obligation_ledger",
|
||||
...toolContractMetadata("build_reconstruction_obligation_ledger"),
|
||||
description:
|
||||
"Generate one deterministic page of a ReconstructionObligationLedger from an authenticated Evidence bundle, reviewed obligations, and an explicit reconstruction manifest. Static candidates remain candidates; duplicate ownership, missing original or reconstruction cases, missing parser/type, weak verifier authority, unenumerated claims, contradictions, dependencies, and residual unknowns fail closed. Every page carries the same full-ledger closure digest and typed per-obligation diagnostics.",
|
||||
"Generate the complete deterministic ReconstructionObligationLedger from an authenticated Evidence bundle, reviewed obligations, and an explicit reconstruction manifest. Static candidates remain candidates; duplicate ownership, missing original or reconstruction cases, missing parser/type, weak verifier authority, unenumerated claims, contradictions, dependencies, and residual unknowns fail closed. The inline result carries the full-ledger closure digest and typed per-obligation diagnostics.",
|
||||
kind: "application",
|
||||
inputSchema: reconstructionObligationLedgerInputSchema,
|
||||
outputSchema: reconstructionObligationLedgerOutputSchema,
|
||||
examples: [
|
||||
{
|
||||
title: "Generate the first obligation-ledger page",
|
||||
title: "Generate the complete obligation ledger",
|
||||
input: {
|
||||
evidence_bundle: {
|
||||
artifacts: [],
|
||||
@@ -323,7 +323,6 @@ export const APPLICATION_TOOL_CONTRACTS = [
|
||||
bindings: [],
|
||||
contradictions: [],
|
||||
},
|
||||
page: { offset: 0, limit: 50 },
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -108,7 +108,6 @@ const compareJavaScriptExportShapesFacts = {
|
||||
compareJavaScriptExportShapesInputSchema.shape.right_module_path,
|
||||
right_export_name:
|
||||
compareJavaScriptExportShapesInputSchema.shape.right_export_name,
|
||||
limits: compareJavaScriptExportShapesInputSchema.shape.limits,
|
||||
unknown_registry_approved:
|
||||
compareJavaScriptExportShapesInputSchema.shape.unknown_registry_approved,
|
||||
} as const;
|
||||
|
||||
@@ -54,8 +54,8 @@ export const TOOL_EXAMPLE_OVERRIDES: Readonly<
|
||||
right_evidence_id: `ev_${"1".repeat(64)}`,
|
||||
},
|
||||
compare_artifacts: {
|
||||
left_evidence_ids: [ARTIFACT_COMPARISON_EXAMPLE.left.evidence_id],
|
||||
right_evidence_ids: [ARTIFACT_COMPARISON_EXAMPLE.right.evidence_id],
|
||||
left_evidence_id: ARTIFACT_COMPARISON_EXAMPLE.left.evidence_id,
|
||||
right_evidence_id: ARTIFACT_COMPARISON_EXAMPLE.right.evidence_id,
|
||||
},
|
||||
compare_functions: {
|
||||
left_evidence_id: FUNCTION_COMPARISON_EXAMPLE.left.evidence_id,
|
||||
|
||||
@@ -70,8 +70,6 @@ export const targetKindSchema = z.enum([
|
||||
|
||||
const providerCapabilityFacts = {
|
||||
operation: z.string(),
|
||||
pagination: z.enum(["none", "offset", "cursor"]),
|
||||
exhaustive: z.boolean(),
|
||||
effects: z.object({
|
||||
mutates_artifact: z.boolean(),
|
||||
launches_process: z.boolean(),
|
||||
@@ -81,11 +79,6 @@ const providerCapabilityFacts = {
|
||||
changes_permissions: z.boolean(),
|
||||
requires_root: z.boolean(),
|
||||
}),
|
||||
limits: z.object({
|
||||
max_results: z.number().int().min(0).nullable(),
|
||||
max_payload_bytes: z.number().int().min(0).nullable(),
|
||||
timeout_ms: z.number().int().min(0).nullable(),
|
||||
}),
|
||||
limitations: z.array(z.string()),
|
||||
};
|
||||
|
||||
@@ -418,26 +411,6 @@ const availableMemoryPermissions = z.object({
|
||||
source: z.literal("ghidra-memory-block"),
|
||||
});
|
||||
|
||||
export const bounded = (item: z.ZodType) => {
|
||||
const facts = {
|
||||
items: z.array(item),
|
||||
total: z.number().int().min(0).nullable(),
|
||||
returned: z.number().int().min(0),
|
||||
} as const;
|
||||
return z.discriminatedUnion("truncated", [
|
||||
z.object({
|
||||
...facts,
|
||||
truncated: z.literal(false),
|
||||
next_offset: z.null(),
|
||||
}),
|
||||
z.object({
|
||||
...facts,
|
||||
truncated: z.literal(true),
|
||||
next_offset: z.number().int().min(0).nullable(),
|
||||
}),
|
||||
]);
|
||||
};
|
||||
|
||||
export const addressedValue = z.object({
|
||||
address: z.string(),
|
||||
value: z.string(),
|
||||
|
||||
@@ -29,11 +29,9 @@ const PROPERTY_DESCRIPTIONS: Readonly<Record<string, string>> = {
|
||||
question: "Concrete unresolved question to retain for later investigation.",
|
||||
result: "Primary structured result returned by this operation.",
|
||||
left: "Left-hand input used for comparison or differential execution.",
|
||||
limit: "Maximum number of results to return in this page.",
|
||||
limits:
|
||||
"Bounded resource-consumption and result-size limits for this operation.",
|
||||
mode: "Operation mode that selects the requested behavior.",
|
||||
offset: "Zero-based index of the first result to return.",
|
||||
overwrite: "Whether an existing destination may be replaced.",
|
||||
path: "Local filesystem path used by this operation.",
|
||||
provider_id:
|
||||
@@ -119,10 +117,6 @@ const fallbackPropertyDescription = (property: string): string => {
|
||||
const words = property.replaceAll("_", " ");
|
||||
if (property.endsWith("_approved"))
|
||||
return `Explicit operator approval for ${words.slice(0, -9)}.`;
|
||||
if (property.endsWith("_offset"))
|
||||
return `Zero-based index of the first ${words.slice(0, -7)} to return.`;
|
||||
if (property.endsWith("_limit"))
|
||||
return `Maximum number of ${words.slice(0, -6)} entries to return.`;
|
||||
if (property.startsWith("max_"))
|
||||
return `Maximum permitted ${words.slice(4)} for this operation.`;
|
||||
if (property.startsWith("include_"))
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
HopperProcessError,
|
||||
HopperStartError,
|
||||
HopperTimeoutError,
|
||||
InvestigationWorkspaceError,
|
||||
WorkspaceStorageError,
|
||||
NoBinaryOpenError,
|
||||
PermissionRequiredError,
|
||||
ProviderSelectionError,
|
||||
@@ -81,8 +81,8 @@ export const analysisErrorCategory = (
|
||||
return artifactErrorCategory(error.reason);
|
||||
if (error instanceof EvidenceFileError && error.reason === "disabled")
|
||||
return "unavailable";
|
||||
if (error instanceof InvestigationWorkspaceError)
|
||||
return investigationWorkspaceCategory(error.reason);
|
||||
if (error instanceof WorkspaceStorageError)
|
||||
return workspaceStorageCategory(error.reason);
|
||||
return STATIC_ERROR_CATEGORIES[error._tag] ?? "execution_failure";
|
||||
};
|
||||
|
||||
@@ -101,8 +101,8 @@ const browserErrorCategory = (
|
||||
return "execution_failure";
|
||||
};
|
||||
|
||||
const investigationWorkspaceCategory = (
|
||||
reason: InvestigationWorkspaceError["reason"],
|
||||
const workspaceStorageCategory = (
|
||||
reason: WorkspaceStorageError["reason"],
|
||||
): AnalysisErrorProjection["category"] => {
|
||||
if (
|
||||
reason === "disabled" ||
|
||||
@@ -162,8 +162,8 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => {
|
||||
return "Evidence is too large for this session. Reduce the evidence set and try again.";
|
||||
if (error instanceof EvidenceFileError)
|
||||
return evidenceFileMessage(error.reason);
|
||||
if (error instanceof InvestigationWorkspaceError)
|
||||
return investigationWorkspaceMessage(error.reason);
|
||||
if (error instanceof WorkspaceStorageError)
|
||||
return workspaceStorageMessage(error.reason);
|
||||
if (error instanceof UnknownRegistryError && error.reason === "not-found")
|
||||
return "The requested residual unknown does not exist in this session. Check the unknown_id and try again.";
|
||||
if (error instanceof UnknownRegistryError)
|
||||
@@ -274,8 +274,8 @@ const evidenceFileMessage = (reason: EvidenceFileError["reason"]): string => {
|
||||
return "Evidence file could not be accessed. Check file permissions and try again.";
|
||||
};
|
||||
|
||||
const investigationWorkspaceMessage = (
|
||||
reason: InvestigationWorkspaceError["reason"],
|
||||
const workspaceStorageMessage = (
|
||||
reason: WorkspaceStorageError["reason"],
|
||||
): string => {
|
||||
if (reason === "disabled")
|
||||
return "Investigation workspace access is disabled. Configure a workspace directory and try again.";
|
||||
@@ -309,7 +309,7 @@ const KNOWN_ERROR_TAGS = {
|
||||
EvidenceIntegrityError: true,
|
||||
EvidenceLimitError: true,
|
||||
EvidenceFileError: true,
|
||||
InvestigationWorkspaceError: true,
|
||||
WorkspaceStorageError: true,
|
||||
UnknownRegistryError: true,
|
||||
HopperTimeoutError: true,
|
||||
HopperCancelledError: true,
|
||||
|
||||
@@ -246,32 +246,6 @@ describe("analysis error projection: caller contract", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("retains schema-authored input guidance", () => {
|
||||
expect(
|
||||
projectAnalysisError(
|
||||
new AnalysisInputError("find_changed_behavior", undefined, [
|
||||
{
|
||||
path: [],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"Supply either existing comparisons or one investigation_run",
|
||||
},
|
||||
]),
|
||||
),
|
||||
).toMatchObject({
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
path: [],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"Supply either existing comparisons or one investigation_run",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("distinguishes disabled integrity continuation from format support", () => {
|
||||
expect(
|
||||
projectAnalysisError(
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
HopperProcessError,
|
||||
HopperRemoteError,
|
||||
HopperTimeoutError,
|
||||
InvestigationWorkspaceError,
|
||||
WorkspaceStorageError,
|
||||
PermissionRequiredError,
|
||||
ProviderAdapterError,
|
||||
ProviderSelectionError,
|
||||
@@ -73,8 +73,8 @@ const errorCode = (error: AnalysisError): AnalysisErrorProjection["code"] => {
|
||||
if (error instanceof ArtifactOperationError)
|
||||
return artifactOperationCode(error);
|
||||
if (error instanceof EvidenceFileError) return evidenceFileCode(error.reason);
|
||||
if (error instanceof InvestigationWorkspaceError)
|
||||
return investigationWorkspaceCode(error.reason);
|
||||
if (error instanceof WorkspaceStorageError)
|
||||
return workspaceStorageCode(error.reason);
|
||||
if (error instanceof UnknownRegistryError)
|
||||
return unknownRegistryCode(error.reason);
|
||||
if (error._tag === "ProcessCaptureError") return processCaptureCode(error);
|
||||
@@ -115,8 +115,8 @@ const evidenceFileCode = (
|
||||
return "execution_failure";
|
||||
};
|
||||
|
||||
const investigationWorkspaceCode = (
|
||||
reason: InvestigationWorkspaceError["reason"],
|
||||
const workspaceStorageCode = (
|
||||
reason: WorkspaceStorageError["reason"],
|
||||
): AnalysisErrorProjection["code"] => {
|
||||
if (reason === "revision-conflict" || reason === "name-conflict")
|
||||
return "revision_conflict";
|
||||
@@ -152,7 +152,7 @@ type SpecializedErrorTag =
|
||||
| "ArtifactOperationError"
|
||||
| "BrowserObservationError"
|
||||
| "EvidenceFileError"
|
||||
| "InvestigationWorkspaceError"
|
||||
| "WorkspaceStorageError"
|
||||
| "PermissionRequiredError"
|
||||
| "ProcessCaptureError"
|
||||
| "ProviderSelectionError"
|
||||
@@ -192,7 +192,7 @@ const staticErrorCode = (
|
||||
case "ArtifactOperationError":
|
||||
case "BrowserObservationError":
|
||||
case "EvidenceFileError":
|
||||
case "InvestigationWorkspaceError":
|
||||
case "WorkspaceStorageError":
|
||||
case "PermissionRequiredError":
|
||||
case "ProcessCaptureError":
|
||||
case "ProviderSelectionError":
|
||||
@@ -268,7 +268,7 @@ const artifactStateErrorDetails = (
|
||||
};
|
||||
if (error instanceof EvidenceLimitError)
|
||||
return { limit: error.limit, maximum: error.maximum, truncated: true };
|
||||
if (error instanceof InvestigationWorkspaceError)
|
||||
if (error instanceof WorkspaceStorageError)
|
||||
return { operation: error.operation, reason: error.reason };
|
||||
if (error instanceof UnknownRegistryError) return { reason: error.reason };
|
||||
if (error instanceof EvidenceFileError)
|
||||
|
||||
@@ -65,9 +65,7 @@ describe("analysis snapshot contract", () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects snapshot v1 with explicit recapture guidance", () => {
|
||||
expect(() => parseAnalysisSnapshot({ snapshot_version: 1 })).toThrow(
|
||||
/v1.*recapture.*v2/iu,
|
||||
);
|
||||
it("rejects an input without the current snapshot shape", () => {
|
||||
expect(() => parseAnalysisSnapshot({ snapshot_version: 1 })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -270,9 +270,8 @@ export const createAnalysisSnapshotEntry = (input: {
|
||||
};
|
||||
};
|
||||
|
||||
/** Parse v2 and reject legacy snapshots, altered query IDs, or non-canonical order. */
|
||||
/** Parse a provider/profile-exact snapshot and verify its entries and evidence. */
|
||||
export const parseAnalysisSnapshot = (input: unknown): AnalysisSnapshot => {
|
||||
rejectLegacySnapshot(input);
|
||||
const parsed = analysisSnapshotSchema.parse(input);
|
||||
parseEvidenceBundle(parsed.evidence_bundle);
|
||||
if (
|
||||
@@ -314,15 +313,3 @@ export const parseAnalysisSnapshot = (input: unknown): AnalysisSnapshot => {
|
||||
/** Serialize a validated snapshot with byte-stable canonical entry ordering. */
|
||||
export const serializeAnalysisSnapshot = (snapshot: AnalysisSnapshot): string =>
|
||||
`${canonicalJson(parseAnalysisSnapshot(snapshot))}\n`;
|
||||
|
||||
const rejectLegacySnapshot = (input: unknown): void => {
|
||||
if (
|
||||
typeof input === "object" &&
|
||||
input !== null &&
|
||||
"snapshot_version" in input &&
|
||||
input.snapshot_version === 1
|
||||
)
|
||||
throw new TypeError(
|
||||
"Analysis snapshot v1 is incompatible with profile-exact replay; recapture it as snapshot v2. Its Evidence bundle may be imported separately.",
|
||||
);
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("bundle comparison", () => {
|
||||
const second = evidence("second");
|
||||
const left = createEvidenceBundle([first, second]);
|
||||
const right = createEvidenceBundle([second, first]);
|
||||
const result = compareBundles(left, right, [], 0, 100);
|
||||
const result = compareBundles(left, right);
|
||||
expect(bundleComparisonResultSchema.parse(result)).toMatchObject({
|
||||
status: "unchanged",
|
||||
summary: {
|
||||
@@ -38,12 +38,12 @@ describe("bundle comparison", () => {
|
||||
records_removed: 0,
|
||||
unresolved: 0,
|
||||
},
|
||||
changes: { total: 0, next_offset: null },
|
||||
changes: [],
|
||||
});
|
||||
expect(result.left_bundle_sha256).toBe(result.right_bundle_sha256);
|
||||
});
|
||||
|
||||
it("classifies explicit pairs and pages stable membership changes", () => {
|
||||
it("classifies explicit pairs and returns every membership change inline", () => {
|
||||
const oldRecord = evidence("old");
|
||||
const newRecord = evidence("new");
|
||||
const removed = evidence("removed");
|
||||
@@ -56,8 +56,8 @@ describe("bundle comparison", () => {
|
||||
right_evidence_id: newRecord.evidence_id,
|
||||
},
|
||||
];
|
||||
const first = compareBundles(left, right, pairs, 0, 1);
|
||||
const repeated = compareBundles(left, right, pairs, 0, 1);
|
||||
const first = compareBundles(left, right, pairs);
|
||||
const repeated = compareBundles(left, right, pairs);
|
||||
expect(first).toEqual(repeated);
|
||||
expect(first).toMatchObject({
|
||||
status: "changed",
|
||||
@@ -66,61 +66,57 @@ describe("bundle comparison", () => {
|
||||
records_removed: 1,
|
||||
records_changed: 1,
|
||||
},
|
||||
changes: { total: 3, next_offset: 1 },
|
||||
changes: expect.arrayContaining([
|
||||
expect.objectContaining({ classification: "changed" }),
|
||||
]),
|
||||
});
|
||||
expect(
|
||||
compareBundles(left, right, pairs, 1, 500).changes.items,
|
||||
).toHaveLength(2);
|
||||
expect(first.changes).toHaveLength(3);
|
||||
expect(first.limitations).toContain(
|
||||
"One-sided membership proves only bundle inclusion or omission, not behavioral absence.",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns more than 500 changes without a display-page ceiling", () => {
|
||||
const records = Array.from({ length: 501 }, (_, index) =>
|
||||
evidence(`record-${String(index)}`),
|
||||
);
|
||||
const result = compareBundles(
|
||||
createEvidenceBundle([]),
|
||||
createEvidenceBundle(records),
|
||||
);
|
||||
expect(result.changes).toHaveLength(501);
|
||||
expect(bundleComparisonResultSchema.parse(result).changes).toHaveLength(
|
||||
501,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects missing and non-bijective explicit pairs", () => {
|
||||
const leftRecord = evidence("left");
|
||||
const rightRecord = evidence("right");
|
||||
const left = createEvidenceBundle([leftRecord]);
|
||||
const right = createEvidenceBundle([rightRecord]);
|
||||
expect(() =>
|
||||
compareBundles(
|
||||
left,
|
||||
right,
|
||||
[
|
||||
{
|
||||
left_evidence_id: evidence("missing").evidence_id,
|
||||
right_evidence_id: rightRecord.evidence_id,
|
||||
},
|
||||
],
|
||||
0,
|
||||
100,
|
||||
),
|
||||
compareBundles(left, right, [
|
||||
{
|
||||
left_evidence_id: evidence("missing").evidence_id,
|
||||
right_evidence_id: rightRecord.evidence_id,
|
||||
},
|
||||
]),
|
||||
).toThrow(/missing left evidence/u);
|
||||
expect(() =>
|
||||
compareBundles(
|
||||
left,
|
||||
right,
|
||||
[
|
||||
{
|
||||
left_evidence_id: leftRecord.evidence_id,
|
||||
right_evidence_id: rightRecord.evidence_id,
|
||||
},
|
||||
{
|
||||
left_evidence_id: leftRecord.evidence_id,
|
||||
right_evidence_id: rightRecord.evidence_id,
|
||||
},
|
||||
],
|
||||
0,
|
||||
100,
|
||||
),
|
||||
compareBundles(left, right, [
|
||||
{
|
||||
left_evidence_id: leftRecord.evidence_id,
|
||||
right_evidence_id: rightRecord.evidence_id,
|
||||
},
|
||||
{
|
||||
left_evidence_id: leftRecord.evidence_id,
|
||||
right_evidence_id: rightRecord.evidence_id,
|
||||
},
|
||||
]),
|
||||
).toThrow(/one-to-one/u);
|
||||
expect(() =>
|
||||
compareBundles(
|
||||
{ ...left, records: [leftRecord, leftRecord] },
|
||||
right,
|
||||
[],
|
||||
0,
|
||||
100,
|
||||
),
|
||||
compareBundles({ ...left, records: [leftRecord, leftRecord] }, right),
|
||||
).toThrow(/duplicate record IDs/u);
|
||||
});
|
||||
});
|
||||
@@ -170,31 +166,27 @@ describe("bundle comparison history", () => {
|
||||
[mutationOne, mutationTwo],
|
||||
[initial, advanced],
|
||||
);
|
||||
expect(
|
||||
compareBundles(initialBundle, advancedBundle, [], 0, 100),
|
||||
).toMatchObject({
|
||||
expect(compareBundles(initialBundle, advancedBundle)).toMatchObject({
|
||||
status: "changed",
|
||||
summary: { unknowns_advanced: 1, unresolved: 0 },
|
||||
changes: {
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
entity: "evidence",
|
||||
classification: "added",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
entity: "residual_unknown",
|
||||
classification: "history_advanced",
|
||||
}),
|
||||
],
|
||||
},
|
||||
changes: [
|
||||
expect.objectContaining({
|
||||
entity: "evidence",
|
||||
classification: "added",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
entity: "residual_unknown",
|
||||
classification: "history_advanced",
|
||||
}),
|
||||
],
|
||||
});
|
||||
const absent = createEvidenceBundle([]);
|
||||
const missing = compareBundles(initialBundle, absent, [], 0, 100);
|
||||
const missing = compareBundles(initialBundle, absent);
|
||||
expect(missing).toMatchObject({
|
||||
status: "unknown",
|
||||
summary: { unknowns_removed: 1, unresolved: 1 },
|
||||
});
|
||||
expect(missing.changes.items).toContainEqual(
|
||||
expect(missing.changes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
classification: "removed",
|
||||
conclusion_kind: "unresolved_branch",
|
||||
|
||||
@@ -20,7 +20,7 @@ const classificationSchema = z.enum([
|
||||
"history_diverged",
|
||||
]);
|
||||
|
||||
/** Strict bounded input for canonical Evidence bundle comparison. */
|
||||
/** Strict input for canonical Evidence bundle comparison. */
|
||||
export const bundleComparisonInputSchema = z.strictObject({
|
||||
left_bundle_path: z.string().min(1).max(4_096),
|
||||
right_bundle_path: z.string().min(1).max(4_096),
|
||||
@@ -31,10 +31,7 @@ export const bundleComparisonInputSchema = z.strictObject({
|
||||
right_evidence_id: evidenceIdSchema,
|
||||
}),
|
||||
)
|
||||
.max(500)
|
||||
.default([]),
|
||||
offset: z.number().int().min(0).default(0),
|
||||
limit: z.number().int().min(1).max(500).default(100),
|
||||
});
|
||||
|
||||
const bundleChangeSchema = z.object({
|
||||
@@ -70,13 +67,7 @@ export const bundleComparisonResultSchema = z.object({
|
||||
unknowns_diverged: z.number().int().min(0),
|
||||
unresolved: z.number().int().min(0),
|
||||
}),
|
||||
changes: z.object({
|
||||
items: z.array(bundleChangeSchema).max(500),
|
||||
offset: z.number().int().min(0),
|
||||
limit: z.number().int().min(1).max(500),
|
||||
total: z.number().int().min(0),
|
||||
next_offset: z.number().int().min(0).nullable(),
|
||||
}),
|
||||
changes: z.array(bundleChangeSchema),
|
||||
limitations: z.array(z.string()),
|
||||
});
|
||||
|
||||
@@ -92,13 +83,9 @@ const MAX_ENTITIES = 100_000;
|
||||
|
||||
/** Compare canonical bundle membership and only explicitly paired observations. */
|
||||
export const compareBundles = (
|
||||
...[leftInput, rightInput, recordPairs, offset, limit]: readonly [
|
||||
unknown,
|
||||
unknown,
|
||||
readonly RecordPair[],
|
||||
number,
|
||||
number,
|
||||
]
|
||||
leftInput: unknown,
|
||||
rightInput: unknown,
|
||||
recordPairs: readonly RecordPair[] = [],
|
||||
): BundleComparisonResult => {
|
||||
const left = parseEvidenceBundle(leftInput);
|
||||
const right = parseEvidenceBundle(rightInput);
|
||||
@@ -110,7 +97,6 @@ export const compareBundles = (
|
||||
const changes = [...recordResult.changes, ...unknownResult.changes].sort(
|
||||
compareChanges,
|
||||
);
|
||||
const page = changes.slice(offset, offset + limit);
|
||||
const unresolved = changes.filter(
|
||||
({ conclusion_kind: kind }) => kind !== "observed_change",
|
||||
).length;
|
||||
@@ -128,14 +114,7 @@ export const compareBundles = (
|
||||
...unknownResult.summary,
|
||||
unresolved,
|
||||
},
|
||||
changes: {
|
||||
items: page,
|
||||
offset,
|
||||
limit,
|
||||
total: changes.length,
|
||||
next_offset:
|
||||
offset + page.length < changes.length ? offset + page.length : null,
|
||||
},
|
||||
changes,
|
||||
limitations: changes.some(({ limitations }) => limitations.length > 0)
|
||||
? [
|
||||
"One-sided membership proves only bundle inclusion or omission, not behavioral absence.",
|
||||
|
||||
@@ -85,11 +85,6 @@ const processResult = (
|
||||
});
|
||||
|
||||
const artifactResult = (
|
||||
pagination: {
|
||||
readonly offset?: number;
|
||||
readonly next_offset?: number | null;
|
||||
readonly total?: number;
|
||||
} = {},
|
||||
nestedLinks: string[] = [left.evidence_id, right.evidence_id],
|
||||
): JsonValue => ({
|
||||
status: "changed",
|
||||
@@ -102,24 +97,18 @@ const artifactResult = (
|
||||
changed: 1,
|
||||
unknown: 0,
|
||||
},
|
||||
changes: {
|
||||
items: [
|
||||
{
|
||||
classification: "changed",
|
||||
logical_path: "main.js",
|
||||
dimensions: ["content"],
|
||||
left_occurrence_id: `occ_${"5".repeat(64)}`,
|
||||
right_occurrence_id: `occ_${"6".repeat(64)}`,
|
||||
left_artifact_id: `art_${"7".repeat(64)}`,
|
||||
right_artifact_id: `art_${"8".repeat(64)}`,
|
||||
evidence_links: nestedLinks,
|
||||
},
|
||||
],
|
||||
offset: pagination.offset ?? 0,
|
||||
limit: 100,
|
||||
total: pagination.total ?? 1,
|
||||
next_offset: pagination.next_offset ?? null,
|
||||
},
|
||||
changes: [
|
||||
{
|
||||
classification: "changed",
|
||||
logical_path: "main.js",
|
||||
dimensions: ["content"],
|
||||
left_occurrence_id: `occ_${"5".repeat(64)}`,
|
||||
right_occurrence_id: `occ_${"6".repeat(64)}`,
|
||||
left_artifact_id: `art_${"7".repeat(64)}`,
|
||||
right_artifact_id: `art_${"8".repeat(64)}`,
|
||||
evidence_links: nestedLinks,
|
||||
},
|
||||
],
|
||||
limitations: [],
|
||||
});
|
||||
|
||||
@@ -134,11 +123,9 @@ const expectInvalidSummary = (result: ChangedBehaviorResult): void => {
|
||||
|
||||
describe("changed behavior", () => {
|
||||
it("accepts process comparison Evidence", () => {
|
||||
const result = findChangedBehavior(
|
||||
[comparison("compare_process_captures", processResult())],
|
||||
0,
|
||||
100,
|
||||
);
|
||||
const result = findChangedBehavior([
|
||||
comparison("compare_process_captures", processResult()),
|
||||
]);
|
||||
expect(result.behavior_status).toBe("observed_unchanged");
|
||||
});
|
||||
|
||||
@@ -152,7 +139,7 @@ describe("changed behavior", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(findChangedBehavior([evidence], 0, 100)).toMatchObject({
|
||||
expect(findChangedBehavior([evidence])).toMatchObject({
|
||||
behavior_status: "observed_changed",
|
||||
summary: { observed_changes: 2 },
|
||||
findings: {
|
||||
@@ -172,11 +159,11 @@ describe("changed behavior", () => {
|
||||
"compare_process_captures",
|
||||
processResult({ status: "changed", terminal: "changed" }),
|
||||
);
|
||||
const result = findChangedBehavior([evidence], 0, 100);
|
||||
const result = findChangedBehavior([evidence]);
|
||||
expect(changedBehaviorResultSchema.parse(result)).toMatchObject({
|
||||
behavior_status: "observed_changed",
|
||||
summary: { observed_changes: 1, static_candidates: 0 },
|
||||
findings: { total: 1, next_offset: null },
|
||||
findings: { items: expect.any(Array) },
|
||||
});
|
||||
expectInvalidSummary(result);
|
||||
expect(result.findings.items[0]).toMatchObject({
|
||||
@@ -193,7 +180,7 @@ describe("changed behavior", () => {
|
||||
|
||||
it("keeps static differences as candidates, never runtime observations", () => {
|
||||
const evidence = comparison("compare_artifacts", artifactResult());
|
||||
const result = findChangedBehavior([evidence], 0, 100);
|
||||
const result = findChangedBehavior([evidence]);
|
||||
expect(result).toMatchObject({
|
||||
behavior_status: "unknown",
|
||||
summary: { observed_changes: 0, static_candidates: 1 },
|
||||
@@ -217,40 +204,26 @@ describe("changed behavior", () => {
|
||||
"compare_process_captures",
|
||||
processResult({ status: "unknown", exit: "unknown" }),
|
||||
);
|
||||
const result = findChangedBehavior([changed, unknown], 0, 1);
|
||||
const result = findChangedBehavior([changed, unknown]);
|
||||
expect(result).toMatchObject({
|
||||
behavior_status: "unknown",
|
||||
findings: { total: 2, limit: 1, next_offset: 1 },
|
||||
findings: { items: expect.any(Array) },
|
||||
});
|
||||
expect(findChangedBehavior([changed, unknown], 0, 1)).toEqual(result);
|
||||
expect(findChangedBehavior([changed, unknown])).toEqual(result);
|
||||
});
|
||||
|
||||
it("keeps incomplete artifact comparison pagination explicit", () => {
|
||||
const paged = comparison(
|
||||
"compare_artifacts",
|
||||
artifactResult({ next_offset: 1, total: 2 }),
|
||||
);
|
||||
const result = findChangedBehavior([paged], 0, 100);
|
||||
expect(result).toMatchObject({
|
||||
behavior_status: "unknown",
|
||||
summary: { static_candidates: 1, unresolved: 1 },
|
||||
findings: { total: 2 },
|
||||
});
|
||||
expect(result.limitations).toContain(
|
||||
"Artifact comparison reports 1 of 2 changes.",
|
||||
);
|
||||
const laterPage = comparison(
|
||||
"compare_artifacts",
|
||||
artifactResult({ offset: 1, next_offset: null, total: 2 }),
|
||||
);
|
||||
expect(() => findChangedBehavior([laterPage], 0, 100)).toThrow(
|
||||
/pagination from offset zero/u,
|
||||
it("uses complete inline artifact deltas as static candidates", () => {
|
||||
const complete = comparison("compare_artifacts", artifactResult());
|
||||
expect(findChangedBehavior([complete]).findings.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ dimension: "artifact:main.js" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects bundles, duplicate comparisons, and malformed exact results", () => {
|
||||
const valid = comparison("compare_process_captures", processResult());
|
||||
expect(() => findChangedBehavior([valid, valid], 0, 100)).toThrow(
|
||||
expect(() => findChangedBehavior([valid, valid])).toThrow(
|
||||
/duplicate comparison Evidence/u,
|
||||
);
|
||||
const bundle = createEvidence(
|
||||
@@ -266,25 +239,25 @@ describe("changed behavior", () => {
|
||||
evidenceLinks: [left.evidence_id, right.evidence_id],
|
||||
},
|
||||
);
|
||||
expect(() => findChangedBehavior([bundle], 0, 100)).toThrow(
|
||||
expect(() => findChangedBehavior([bundle])).toThrow(
|
||||
/requires process, artifact, or function/u,
|
||||
);
|
||||
const malformed = comparison("compare_process_captures", {
|
||||
status: "unchanged",
|
||||
});
|
||||
expect(() => findChangedBehavior([malformed], 0, 100)).toThrow();
|
||||
expect(() => findChangedBehavior([malformed])).toThrow();
|
||||
const contradictory = comparison(
|
||||
"compare_process_captures",
|
||||
processResult({ status: "unchanged", terminal: "changed" }),
|
||||
);
|
||||
expect(() => findChangedBehavior([contradictory], 0, 100)).toThrow(
|
||||
expect(() => findChangedBehavior([contradictory])).toThrow(
|
||||
/contradicts its dimensions/u,
|
||||
);
|
||||
const danglingNested = comparison(
|
||||
"compare_artifacts",
|
||||
artifactResult({}, [`ev_${"f".repeat(64)}`, right.evidence_id]),
|
||||
artifactResult([`ev_${"f".repeat(64)}`, right.evidence_id]),
|
||||
);
|
||||
expect(() => findChangedBehavior([danglingNested], 0, 100)).toThrow(
|
||||
expect(() => findChangedBehavior([danglingNested])).toThrow(
|
||||
/outside its top-level closure/u,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,33 +9,14 @@ import {
|
||||
PROCESS_COMPARISON_DIMENSIONS,
|
||||
processCaptureComparisonSchema,
|
||||
} from "./processCapture.js";
|
||||
import {
|
||||
crossVersionInvestigationInputSchema,
|
||||
investigationRunSummarySchema,
|
||||
} from "./investigationWorkspace.js";
|
||||
|
||||
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
|
||||
|
||||
/** Strict bounded input for aggregating existing comparison Evidence. */
|
||||
export const changedBehaviorInputSchema = z
|
||||
.object({
|
||||
comparisons: z.array(evidenceSchema).max(100).default([]),
|
||||
investigation_run: crossVersionInvestigationInputSchema.optional(),
|
||||
offset: z.number().int().min(0).default(0),
|
||||
limit: z.number().int().min(1).max(100).default(100),
|
||||
unknown_registry_approved: z.literal(true).optional(),
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
if (
|
||||
input.comparisons.length > 0 ===
|
||||
(input.investigation_run !== undefined)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message:
|
||||
"Supply either existing comparisons or one investigation_run, but not both",
|
||||
});
|
||||
});
|
||||
export const changedBehaviorInputSchema = z.object({
|
||||
comparisons: z.array(evidenceSchema).max(100).default([]),
|
||||
unknown_registry_approved: z.literal(true).optional(),
|
||||
});
|
||||
|
||||
const findingSchema = z.object({
|
||||
scope: z.enum(["runtime", "protocol", "resource", "static_candidate"]),
|
||||
@@ -68,47 +49,22 @@ export const changedBehaviorResultSchema = z
|
||||
unresolved: z.number().int().min(0),
|
||||
}),
|
||||
findings: z.object({
|
||||
items: z.array(findingSchema).max(100),
|
||||
offset: z.number().int().min(0),
|
||||
limit: z.number().int().min(1).max(100),
|
||||
total: z.number().int().min(0),
|
||||
next_offset: z.number().int().min(0).nullable(),
|
||||
items: z.array(findingSchema),
|
||||
}),
|
||||
evidence_links: z.array(evidenceIdSchema).min(3).max(20_100),
|
||||
limitations: z.array(z.string()),
|
||||
investigation_run: investigationRunSummarySchema.optional(),
|
||||
})
|
||||
.superRefine((result, context) => {
|
||||
const summaryTotal = Object.values(result.summary).reduce(
|
||||
(total, count) => total + count,
|
||||
0,
|
||||
);
|
||||
const expectedNextOffset =
|
||||
result.findings.offset + result.findings.items.length <
|
||||
result.findings.total
|
||||
? result.findings.offset + result.findings.items.length
|
||||
: null;
|
||||
if (summaryTotal !== result.findings.total)
|
||||
if (summaryTotal !== result.findings.items.length)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Finding classification counts must equal the finding total",
|
||||
path: ["summary"],
|
||||
});
|
||||
if (result.findings.next_offset !== expectedNextOffset)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Finding cursor must identify the next retained offset",
|
||||
path: ["findings", "next_offset"],
|
||||
});
|
||||
if (
|
||||
result.findings.offset < result.findings.total &&
|
||||
result.findings.items.length === 0
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "A nonterminal finding page must retain at least one item",
|
||||
path: ["findings", "items"],
|
||||
});
|
||||
if (
|
||||
(result.behavior_status === "observed_changed" &&
|
||||
result.summary.observed_changes === 0) ||
|
||||
@@ -155,13 +111,9 @@ const acceptsPredicate = (
|
||||
/** Find observed runtime changes and separately report static change candidates. */
|
||||
export const findChangedBehavior = (
|
||||
comparisonsInput: unknown,
|
||||
offset: number,
|
||||
limit: number,
|
||||
): ChangedBehaviorResult => {
|
||||
const parsedInput = changedBehaviorInputSchema.parse({
|
||||
comparisons: comparisonsInput,
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
const comparisons = parsedInput.comparisons;
|
||||
const evidence = comparisons.map(parseComparisonEvidence);
|
||||
@@ -176,10 +128,6 @@ export const findChangedBehavior = (
|
||||
);
|
||||
if (links.length > 20_100)
|
||||
throw new TypeError("Changed-behavior Evidence closure exceeds limit");
|
||||
const page = findings.slice(
|
||||
parsedInput.offset,
|
||||
parsedInput.offset + parsedInput.limit,
|
||||
);
|
||||
return changedBehaviorResultSchema.parse({
|
||||
behavior_status: behaviorStatus(runtimeStatuses),
|
||||
summary: {
|
||||
@@ -189,14 +137,7 @@ export const findChangedBehavior = (
|
||||
unresolved: count(findings, "unresolved_branch"),
|
||||
},
|
||||
findings: {
|
||||
items: page,
|
||||
offset: parsedInput.offset,
|
||||
limit: parsedInput.limit,
|
||||
total: findings.length,
|
||||
next_offset:
|
||||
parsedInput.offset + page.length < findings.length
|
||||
? parsedInput.offset + page.length
|
||||
: null,
|
||||
items: findings,
|
||||
},
|
||||
evidence_links: links,
|
||||
limitations: uniqueSorted([
|
||||
@@ -211,7 +152,6 @@ export const findChangedBehavior = (
|
||||
...(runtimeStatuses.length === 0
|
||||
? ["No process comparison Evidence was supplied."]
|
||||
: []),
|
||||
...evidence.flatMap(artifactPaginationLimitations),
|
||||
]),
|
||||
});
|
||||
};
|
||||
@@ -278,13 +218,9 @@ const parseResult = (evidence: Evidence): void => {
|
||||
const result = artifactComparisonResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
if (result.changes.offset !== 0)
|
||||
throw new TypeError(
|
||||
"Changed-behavior analysis requires artifact comparison pagination from offset zero",
|
||||
);
|
||||
assertNestedLinks(
|
||||
evidence,
|
||||
result.changes.items.flatMap(({ evidence_links: links }) => links),
|
||||
result.changes.flatMap(({ evidence_links: links }) => links),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -366,7 +302,7 @@ const artifactFindings = (evidence: Evidence): Finding[] => {
|
||||
const result = artifactComparisonResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
const changes = result.changes.items.map((change) =>
|
||||
const changes = result.changes.map((change) =>
|
||||
makeFinding({
|
||||
evidence,
|
||||
dimension: `artifact:${change.logical_path}`,
|
||||
@@ -382,9 +318,7 @@ const artifactFindings = (evidence: Evidence): Finding[] => {
|
||||
links: change.evidence_links,
|
||||
}),
|
||||
);
|
||||
return (result.status === "unknown" ||
|
||||
result.status === "truncated" ||
|
||||
result.changes.next_offset !== null) &&
|
||||
return (result.status === "unknown" || result.status === "truncated") &&
|
||||
!changes.some(
|
||||
({ classification }) => classification === "unresolved_branch",
|
||||
)
|
||||
@@ -402,18 +336,6 @@ const artifactFindings = (evidence: Evidence): Finding[] => {
|
||||
: changes;
|
||||
};
|
||||
|
||||
const artifactPaginationLimitations = (evidence: Evidence): string[] => {
|
||||
if (evidence.operation !== "compare_artifacts") return [];
|
||||
const result = artifactComparisonResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
return result.changes.next_offset === null
|
||||
? []
|
||||
: [
|
||||
`Artifact comparison reports ${String(result.changes.items.length)} of ${String(result.changes.total)} changes.`,
|
||||
];
|
||||
};
|
||||
|
||||
const makeFinding = (input: {
|
||||
readonly evidence: Evidence;
|
||||
readonly dimension: string;
|
||||
|
||||
@@ -12,16 +12,13 @@ const digest = (digit: string): string => digit.repeat(64);
|
||||
const evidenceId = (digit: string): string => `ev_${digest(digit)}`;
|
||||
|
||||
const report = (): CompletionVerifierReport => ({
|
||||
schema_version: 1,
|
||||
verifier: { id: "managed-conformance", version: "1" },
|
||||
verifier_run: {
|
||||
schema_version: 1,
|
||||
run_id: "11111111-1111-4111-8111-111111111111",
|
||||
verifier_pid: 100,
|
||||
parent_pid: 1,
|
||||
process_lineage: {
|
||||
status: "verified",
|
||||
schema_version: 1,
|
||||
observed_at: "2026-07-22T10:00:00.000Z",
|
||||
launcher_pid: 100,
|
||||
launcher_parent_pid: 1,
|
||||
@@ -41,7 +38,6 @@ const report = (): CompletionVerifierReport => ({
|
||||
scenario: { id: "inspect-managed", version: 1 },
|
||||
artifact_sha256s: [digest("1")],
|
||||
provider: { id: "managed-static", version: "2" },
|
||||
result_schema_version: 1,
|
||||
status: "pass",
|
||||
evidence_ids: [evidenceId("1")],
|
||||
},
|
||||
@@ -50,7 +46,6 @@ const report = (): CompletionVerifierReport => ({
|
||||
scenario: { id: "external-oracle", version: 1 },
|
||||
artifact_sha256s: [],
|
||||
provider: { id: "managed-static", version: "2" },
|
||||
result_schema_version: 1,
|
||||
status: "unsupported",
|
||||
evidence_ids: [evidenceId("2")],
|
||||
},
|
||||
@@ -86,7 +81,6 @@ describe("completion ledger generation", () => {
|
||||
artifact_sha256s: [digest("1")],
|
||||
scenario: { id: "inspect-managed", version: 1 },
|
||||
provider: { id: "managed-static", version: "2" },
|
||||
result_schema_version: 1,
|
||||
});
|
||||
expect(generated.manifest.claims[0]).not.toHaveProperty("status");
|
||||
expect(generated.manifest.claims[0]).not.toHaveProperty("evidence_ids");
|
||||
@@ -142,7 +136,6 @@ describe("completion verifier report validation", () => {
|
||||
...input.verifier_run,
|
||||
process_lineage: {
|
||||
status: "verified",
|
||||
schema_version: 1,
|
||||
observed_at: "2026-07-22T10:00:00.000Z",
|
||||
launcher_pid: 100,
|
||||
launcher_parent_pid: 1,
|
||||
@@ -177,12 +170,6 @@ describe("completion ledger commitment drift", () => {
|
||||
firstClaim(input).provider.version = "3";
|
||||
},
|
||||
],
|
||||
[
|
||||
"result schema",
|
||||
(input: CompletionVerifierReport) => {
|
||||
firstClaim(input).result_schema_version = 2;
|
||||
},
|
||||
],
|
||||
[
|
||||
"environment",
|
||||
(input: CompletionVerifierReport) => {
|
||||
|
||||
@@ -31,14 +31,12 @@ const skillDigestSchema = z.strictObject({
|
||||
});
|
||||
const verifierRunSchema = z
|
||||
.strictObject({
|
||||
schema_version: z.literal(1),
|
||||
run_id: z.string().uuid(),
|
||||
verifier_pid: z.number().int().positive(),
|
||||
parent_pid: z.number().int().nonnegative(),
|
||||
process_lineage: z.discriminatedUnion("status", [
|
||||
z.strictObject({
|
||||
status: z.literal("verified"),
|
||||
schema_version: z.literal(1),
|
||||
observed_at: z.iso.datetime(),
|
||||
launcher_pid: z.number().int().positive(),
|
||||
launcher_parent_pid: z.number().int().nonnegative(),
|
||||
@@ -80,13 +78,11 @@ const reportClaimSchema = z.strictObject({
|
||||
scenario: scenarioSchema,
|
||||
artifact_sha256s: z.array(digestSchema).max(100),
|
||||
provider: identitySchema,
|
||||
result_schema_version: z.number().int().positive(),
|
||||
status: evidenceCompletionRecordSchema.shape.status,
|
||||
evidence_ids: evidenceCompletionRecordSchema.shape.evidence_ids,
|
||||
});
|
||||
|
||||
const completionVerifierReportObjectSchema = z.strictObject({
|
||||
schema_version: z.literal(1),
|
||||
verifier: identitySchema,
|
||||
verifier_run: verifierRunSchema,
|
||||
environment: environmentSchema,
|
||||
@@ -114,7 +110,6 @@ const manifestClaimSchema = reportClaimSchema.omit({
|
||||
evidence_ids: true,
|
||||
});
|
||||
const completionManifestObjectSchema = z.strictObject({
|
||||
schema_version: z.literal(1),
|
||||
manifest_id: z.string().regex(/^ecm_[a-f0-9]{64}$/u),
|
||||
verifier: identitySchema,
|
||||
environment: environmentSchema,
|
||||
@@ -222,7 +217,6 @@ export const createCompletionLedgerArtifacts = (
|
||||
if (!isSortedUnique(skills.map(({ skill_id }) => skill_id)))
|
||||
throw new TypeError("Completion skill IDs must be unique");
|
||||
const projection = {
|
||||
schema_version: 1 as const,
|
||||
verifier: report.verifier,
|
||||
environment: report.environment,
|
||||
skill_digests: skills,
|
||||
|
||||
@@ -7,9 +7,6 @@ import { evidenceEnvelopeSchema } from "./evidence.js";
|
||||
import { evidenceBundleSchema } from "./evidenceBundle.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
|
||||
/** Schema version for the conformance package format. */
|
||||
export const CONFORMANCE_PACKAGE_VERSION = 1;
|
||||
|
||||
const conformancePackageIdSchema = z.string().regex(/^cp_[a-f0-9]{64}$/u);
|
||||
|
||||
const scenarioIdSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9._-]{0,99}$/u);
|
||||
@@ -87,7 +84,6 @@ const verifierContractSchema = z.strictObject({
|
||||
});
|
||||
|
||||
const conformancePackageContentsSchema = z.strictObject({
|
||||
schema_version: z.literal(CONFORMANCE_PACKAGE_VERSION),
|
||||
name: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
created_at: z.string().datetime(),
|
||||
@@ -311,7 +307,6 @@ const parseScenarioSection = (
|
||||
const packageContents = (
|
||||
pkg: ConformancePackageRecord,
|
||||
): ConformancePackageContents => ({
|
||||
schema_version: pkg.schema_version,
|
||||
name: pkg.name,
|
||||
description: pkg.description,
|
||||
created_at: pkg.created_at,
|
||||
|
||||
@@ -33,7 +33,6 @@ const completionSummarySchema = z.strictObject({
|
||||
});
|
||||
|
||||
const completionLedgerObjectSchema = z.strictObject({
|
||||
schema_version: z.literal(1),
|
||||
ledger_id: z.string().regex(/^ecl_[a-f0-9]{64}$/u),
|
||||
records: z.array(evidenceCompletionRecordSchema).min(1).max(10_000),
|
||||
summary: completionSummarySchema,
|
||||
@@ -95,7 +94,7 @@ const computeLedgerId = (
|
||||
records: readonly EvidenceCompletionRecord[],
|
||||
): string =>
|
||||
`ecl_${createHash("sha256")
|
||||
.update(canonicalJson({ schema_version: 1, records }))
|
||||
.update(canonicalJson({ records }))
|
||||
.digest("hex")}`;
|
||||
|
||||
const ledgerIssues = (
|
||||
@@ -156,7 +155,6 @@ export const createEvidenceCompletionLedger = (
|
||||
if (!isSortedUnique(record.evidence_ids))
|
||||
throw new TypeError("Completion Evidence IDs must be unique per claim");
|
||||
return evidenceCompletionLedgerSchema.parse({
|
||||
schema_version: 1,
|
||||
ledger_id: computeLedgerId(records),
|
||||
records,
|
||||
summary: summarize(records),
|
||||
|
||||
@@ -64,7 +64,6 @@ const failures = [
|
||||
export type HopperStartupFailureCode = (typeof failures)[number][1];
|
||||
|
||||
interface HopperStartupDiagnosticContext {
|
||||
readonly schema_version: 1;
|
||||
readonly component: "hopper_private_display";
|
||||
readonly operation: "probe" | "launch";
|
||||
readonly reason: string;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
|
||||
export const ghidraFunctionClassification = () => ({
|
||||
external: false,
|
||||
thunk: false,
|
||||
@@ -40,14 +38,6 @@ export const ghidraReferenceEdge = () => ({
|
||||
kind: ghidraReferenceKind(),
|
||||
});
|
||||
|
||||
export const ghidraBounded = (items: readonly JsonValue[]) => ({
|
||||
items: [...items],
|
||||
total: items.length,
|
||||
returned: items.length,
|
||||
truncated: false,
|
||||
next_offset: null,
|
||||
});
|
||||
|
||||
export const ghidraNativeApiBoundary = (mappingsTruncated = false) => ({
|
||||
available: true as const,
|
||||
provenance: "ghidra-high-function",
|
||||
@@ -173,3 +163,4 @@ export const ghidraFunctionDossier = (
|
||||
],
|
||||
};
|
||||
};
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
|
||||
@@ -16,7 +16,6 @@ export const javascriptExportInstrumentationInputSchema = z.strictObject({
|
||||
});
|
||||
|
||||
export const javascriptExportTransformationManifestSchema = z.strictObject({
|
||||
schema_version: z.literal(1),
|
||||
kind: z.literal("commonjs-factory-export-v1"),
|
||||
artifact_path: z.string().min(1).max(4_096),
|
||||
original_sha256: digestSchema,
|
||||
@@ -77,7 +76,6 @@ export const instrumentJavaScriptExport = (
|
||||
const bindingStart = prefix.byteLength + source.byteLength;
|
||||
const suffixStart = bindingStart + binding.byteLength;
|
||||
const manifest = javascriptExportTransformationManifestSchema.parse({
|
||||
schema_version: 1,
|
||||
kind: "commonjs-factory-export-v1",
|
||||
artifact_path: input.artifact_path,
|
||||
original_sha256: input.artifact_sha256,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { compareCodePoints } from "./javascriptApplicationGraph.js";
|
||||
import { digestExportShapeValue } from "./javascriptExportShapeComparisonIdentity.js";
|
||||
import {
|
||||
javaScriptExportShapeComparisonResultSchema,
|
||||
type CompareJavaScriptExportShapesInput,
|
||||
type JavaScriptExportShapeComparisonChange,
|
||||
type JavaScriptExportShapeComparisonResult,
|
||||
} from "./javascriptExportShapeComparisonSchemas.js";
|
||||
@@ -25,29 +24,16 @@ import {
|
||||
export interface JavaScriptExportShapeComparisonProjectionInput {
|
||||
readonly left: JavaScriptExportShapeSideInput;
|
||||
readonly right: JavaScriptExportShapeSideInput;
|
||||
readonly limits: CompareJavaScriptExportShapesInput["limits"];
|
||||
}
|
||||
|
||||
/** Compare exact selected export return shapes without executing JavaScript. */
|
||||
export const compareJavaScriptExportShapes = (
|
||||
input: JavaScriptExportShapeComparisonProjectionInput,
|
||||
): JavaScriptExportShapeComparisonResult => {
|
||||
const left = selectJavaScriptExport(
|
||||
input.left,
|
||||
input.limits.max_candidate_exports,
|
||||
);
|
||||
const right = selectJavaScriptExport(
|
||||
input.right,
|
||||
input.limits.max_candidate_exports,
|
||||
);
|
||||
const leftRetained = retainJavaScriptExportShapes(
|
||||
left,
|
||||
input.limits.max_return_variants,
|
||||
);
|
||||
const rightRetained = retainJavaScriptExportShapes(
|
||||
right,
|
||||
input.limits.max_return_variants,
|
||||
);
|
||||
const left = selectJavaScriptExport(input.left);
|
||||
const right = selectJavaScriptExport(input.right);
|
||||
const leftRetained = retainJavaScriptExportShapes(left);
|
||||
const rightRetained = retainJavaScriptExportShapes(right);
|
||||
const pairing = pairJavaScriptExportShapeVariants(
|
||||
leftRetained.shapes,
|
||||
rightRetained.shapes,
|
||||
@@ -64,8 +50,8 @@ export const compareJavaScriptExportShapes = (
|
||||
rightShapes: rightRetained.shapes,
|
||||
evidenceLinks,
|
||||
}).sort(compareJavaScriptExportShapeChanges);
|
||||
const changes = allChanges.slice(0, input.limits.max_changes);
|
||||
const omittedChanges = allChanges.length - changes.length;
|
||||
const changes = allChanges;
|
||||
const omittedChanges = 0;
|
||||
const coverage = comparisonCoverage({
|
||||
input,
|
||||
left,
|
||||
|
||||
@@ -54,13 +54,7 @@ const sourceRangeSchema = z
|
||||
});
|
||||
});
|
||||
|
||||
const comparisonLimitsSchema = z.strictObject({
|
||||
max_candidate_exports: z.number().int().min(1).max(1_000).default(100),
|
||||
max_return_variants: z.number().int().min(1).max(1_000).default(128),
|
||||
max_changes: z.number().int().min(1).max(10_000).default(1_000),
|
||||
});
|
||||
|
||||
/** Two authenticated application graphs, exact export selectors, and bounds. */
|
||||
/** Two authenticated application graphs and exact export selectors. */
|
||||
export const compareJavaScriptExportShapesInputSchema = z
|
||||
.strictObject({
|
||||
left: evidenceSchema,
|
||||
@@ -69,11 +63,6 @@ export const compareJavaScriptExportShapesInputSchema = z
|
||||
left_export_name: selectorTextSchema,
|
||||
right_module_path: selectorTextSchema,
|
||||
right_export_name: selectorTextSchema,
|
||||
limits: comparisonLimitsSchema.default({
|
||||
max_candidate_exports: 100,
|
||||
max_return_variants: 128,
|
||||
max_changes: 1_000,
|
||||
}),
|
||||
unknown_registry_approved: z.literal(true).optional(),
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
@@ -304,9 +293,6 @@ export const javaScriptExportShapeComparisonResultSchema = z.strictObject({
|
||||
}),
|
||||
});
|
||||
|
||||
export type CompareJavaScriptExportShapesInput = z.output<
|
||||
typeof compareJavaScriptExportShapesInputSchema
|
||||
>;
|
||||
export type ProjectedExportReturnShapes = z.output<
|
||||
typeof projectedExportReturnShapesSchema
|
||||
>;
|
||||
|
||||
@@ -53,7 +53,7 @@ export type SelectedJavaScriptExport =
|
||||
readonly limitations: readonly string[];
|
||||
};
|
||||
|
||||
/** Caller-retained return shapes and their exact caller-limit omission count. */
|
||||
/** Complete return-shape inventory retained from an authenticated projection. */
|
||||
export interface RetainedJavaScriptExportShapes {
|
||||
readonly shapes: readonly Shape[];
|
||||
readonly omitted: number;
|
||||
@@ -62,7 +62,6 @@ export interface RetainedJavaScriptExportShapes {
|
||||
/** Select one exact trusted export and its exact static return-shape observation. */
|
||||
export const selectJavaScriptExport = (
|
||||
side: JavaScriptExportShapeSideInput,
|
||||
maxCandidates: number,
|
||||
): SelectedJavaScriptExport => {
|
||||
const candidates = exportCandidates(side.graph);
|
||||
const exact = candidates.filter(
|
||||
@@ -79,8 +78,7 @@ export const selectJavaScriptExport = (
|
||||
: matchingOneSelector.length > 0
|
||||
? matchingOneSelector
|
||||
: candidates;
|
||||
const retainedCandidates = diagnosticPool.slice(0, maxCandidates);
|
||||
const base = selectorBase(side, diagnosticPool, retainedCandidates);
|
||||
const base = selectorBase(side, diagnosticPool);
|
||||
if (exact.length === 0)
|
||||
return unresolvedSelection(
|
||||
base,
|
||||
@@ -128,33 +126,30 @@ export const selectJavaScriptExport = (
|
||||
};
|
||||
};
|
||||
|
||||
/** Apply the caller's explicit return-variant limit. */
|
||||
/** Retain every projected return variant. */
|
||||
export const retainJavaScriptExportShapes = (
|
||||
selection: SelectedJavaScriptExport,
|
||||
maximum: number,
|
||||
): RetainedJavaScriptExportShapes => {
|
||||
const shapes = selection.projection?.static_return_shapes ?? [];
|
||||
const retained = shapes.slice(0, maximum);
|
||||
return { shapes: retained, omitted: shapes.length - retained.length };
|
||||
return { shapes, omitted: 0 };
|
||||
};
|
||||
|
||||
const selectorBase = (
|
||||
side: JavaScriptExportShapeSideInput,
|
||||
candidates: readonly ExportCandidate[],
|
||||
retained: readonly ExportCandidate[],
|
||||
): SelectorBase => ({
|
||||
evidence_id: side.evidenceId,
|
||||
graph_id: side.graph.graph_id,
|
||||
requested_module_path: side.modulePath,
|
||||
requested_export_name: side.exportName,
|
||||
candidates: retained.map((candidate) => ({
|
||||
candidates: candidates.map((candidate) => ({
|
||||
node_id: candidate.node.node_id,
|
||||
module_path: candidate.modulePath,
|
||||
export_name: candidate.exportName,
|
||||
matches_requested_module: candidate.modulePath === side.modulePath,
|
||||
matches_requested_export: candidate.exportName === side.exportName,
|
||||
})),
|
||||
omitted_candidates: candidates.length - retained.length,
|
||||
omitted_candidates: 0,
|
||||
});
|
||||
|
||||
const unresolvedSelection = (
|
||||
|
||||
@@ -214,7 +214,6 @@ const runtimeFileIdentitySchema = z
|
||||
|
||||
const replayPlanSchema = z
|
||||
.object({
|
||||
schema_version: z.literal(1),
|
||||
plan_digest: digestSchema,
|
||||
policy_version: z.literal("linux-bwrap-systemd-v1"),
|
||||
policy_sha256: digestSchema,
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
type JavaScriptSemanticGraphNode,
|
||||
} from "./javascriptSemanticGraph.js";
|
||||
import { queryJavaScriptSemanticGraph } from "./javascriptSemanticQuery.js";
|
||||
import { javaScriptSemanticQueryInputSchema } from "./javascriptSemanticQuerySchemas.js";
|
||||
|
||||
const SHA = "a".repeat(64);
|
||||
const JAG_ID = `jag_${"b".repeat(64)}`;
|
||||
@@ -244,70 +243,35 @@ const graphWithCandidateEdge = (): JavaScriptSemanticGraph => {
|
||||
});
|
||||
};
|
||||
|
||||
const graphWithDuplicateLiteral = (): JavaScriptSemanticGraph => {
|
||||
const graph = fixtureGraph();
|
||||
const duplicate = node("literal", "literal-copy", { value: "TOKEN" });
|
||||
const { graph_id: _graphId, ...input } = graph;
|
||||
return createJavaScriptSemanticGraph({
|
||||
...input,
|
||||
nodes: [...graph.nodes, duplicate],
|
||||
});
|
||||
};
|
||||
|
||||
it("bounds relevant unknown frontiers independently from relation pages", () => {
|
||||
it("returns every relevant unknown inline", () => {
|
||||
const graph = fixtureGraph(true);
|
||||
const callable = graph.nodes.find(({ kind }) => kind === "function");
|
||||
if (callable === undefined) throw new Error("Expected callable node");
|
||||
const result = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "semantic-node", node_id: callable.node_id },
|
||||
direction: "forward-influence",
|
||||
limits: { max_unknowns: 0 },
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
status: "truncated",
|
||||
unknowns: [],
|
||||
summary: { relevant_unknowns: 1, retained_unknowns: 0 },
|
||||
coverage: { status: "truncated" },
|
||||
summary: { relevant_unknowns: 1, retained_unknowns: 1 },
|
||||
});
|
||||
expect(result.unknowns).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("traces deterministic forward influence and pages without changing identity", () => {
|
||||
it("returns the complete deterministic forward influence result inline", () => {
|
||||
const graph = fixtureGraph();
|
||||
const input = {
|
||||
seed: { kind: "literal" as const, value: "TOKEN" },
|
||||
direction: "forward-influence" as const,
|
||||
expected: { role: "sink" as const, classes: ["request" as const] },
|
||||
limits: {
|
||||
max_seed_matches: 25,
|
||||
max_nodes: 20,
|
||||
max_edges: 20,
|
||||
max_depth: 12,
|
||||
max_functions: 10,
|
||||
max_modules: 10,
|
||||
page_size: 2,
|
||||
},
|
||||
};
|
||||
const first = queryJavaScriptSemanticGraph(graph, input);
|
||||
expect(first).toMatchObject({
|
||||
status: "found",
|
||||
summary: { traversed_nodes: 4, traversed_relations: 3 },
|
||||
page: { offset: 0, size: 2 },
|
||||
relations: expect.any(Array),
|
||||
});
|
||||
expect(first.expected_match_node_ids).toHaveLength(1);
|
||||
expect(first.page.next_cursor).not.toBeNull();
|
||||
const second = queryJavaScriptSemanticGraph(graph, {
|
||||
...input,
|
||||
cursor: first.page.next_cursor,
|
||||
});
|
||||
expect(second.query_id).toBe(first.query_id);
|
||||
expect(second.page).toMatchObject({
|
||||
offset: 2,
|
||||
size: 1,
|
||||
next_cursor: null,
|
||||
});
|
||||
expect(second.relations[0]?.relation_id).not.toBe(
|
||||
first.relations[0]?.relation_id,
|
||||
);
|
||||
expect(first.relations).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("keeps relevant dynamic frontiers unknown", () => {
|
||||
@@ -343,103 +307,3 @@ it("excludes candidate edges unless the caller explicitly opts in", () => {
|
||||
{ relation: "supplies-request-field", resolution: "candidate" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("bounds seed ambiguity before traversal", () => {
|
||||
const result = queryJavaScriptSemanticGraph(graphWithDuplicateLiteral(), {
|
||||
seed: { kind: "literal", value: "TOKEN" },
|
||||
direction: "forward-influence",
|
||||
limits: {
|
||||
max_seed_matches: 1,
|
||||
max_nodes: 20,
|
||||
max_edges: 20,
|
||||
max_depth: 12,
|
||||
max_functions: 10,
|
||||
max_modules: 10,
|
||||
page_size: 10,
|
||||
},
|
||||
});
|
||||
expect(result.summary).toMatchObject({
|
||||
total_seed_matches: 2,
|
||||
retained_seed_matches: 1,
|
||||
});
|
||||
expect(result.status).toBe("truncated");
|
||||
expect(result.coverage.frontier).toContainEqual(
|
||||
expect.objectContaining({ reason: "max-seed-matches", depth: 0 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports exact caller limits and a deterministic truncation frontier", () => {
|
||||
const result = queryJavaScriptSemanticGraph(fixtureGraph(), {
|
||||
seed: { kind: "literal", value: "TOKEN" },
|
||||
direction: "forward-influence",
|
||||
limits: {
|
||||
max_seed_matches: 1,
|
||||
max_nodes: 2,
|
||||
max_edges: 10,
|
||||
max_depth: 12,
|
||||
max_functions: 10,
|
||||
max_modules: 10,
|
||||
page_size: 10,
|
||||
},
|
||||
});
|
||||
expect(result.status).toBe("truncated");
|
||||
expect(result.coverage.frontier).toMatchObject([{ reason: "max-nodes" }]);
|
||||
expect(result.applied_limits.max_nodes).toBe(2);
|
||||
expect(result.accepted_limit_ranges.max_nodes).toEqual({
|
||||
minimum: 1,
|
||||
maximum: 50_000,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
limit: { max_edges: 1 },
|
||||
reason: "max-edges",
|
||||
},
|
||||
{
|
||||
limit: { max_depth: 0 },
|
||||
reason: "max-depth",
|
||||
},
|
||||
])("reports the $reason frontier", ({ limit, reason }) => {
|
||||
const result = queryJavaScriptSemanticGraph(fixtureGraph(), {
|
||||
seed: { kind: "literal", value: "TOKEN" },
|
||||
direction: "forward-influence",
|
||||
limits: {
|
||||
max_seed_matches: 10,
|
||||
max_nodes: 20,
|
||||
max_edges: limit.max_edges ?? 20,
|
||||
max_depth: limit.max_depth ?? 12,
|
||||
max_functions: 10,
|
||||
max_modules: 10,
|
||||
page_size: 10,
|
||||
},
|
||||
});
|
||||
expect(result.status).toBe("truncated");
|
||||
expect(result.coverage.frontier).toContainEqual(
|
||||
expect.objectContaining({ reason }),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects out-of-range limits and cursors from another query", () => {
|
||||
expect(() =>
|
||||
javaScriptSemanticQueryInputSchema.parse({
|
||||
seed: { kind: "literal", value: "TOKEN" },
|
||||
direction: "forward-influence",
|
||||
limits: { max_nodes: 50_001 },
|
||||
}),
|
||||
).toThrow();
|
||||
const graph = fixtureGraph();
|
||||
const first = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "literal", value: "TOKEN" },
|
||||
direction: "forward-influence",
|
||||
limits: { page_size: 1 },
|
||||
});
|
||||
expect(() =>
|
||||
queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "endpoint", value: "https://example.invalid/v1" },
|
||||
direction: "backward-provenance",
|
||||
limits: { page_size: 1 },
|
||||
cursor: first.page.next_cursor,
|
||||
}),
|
||||
).toThrow(/cursor does not match/u);
|
||||
});
|
||||
|
||||
@@ -18,13 +18,20 @@ import {
|
||||
} from "./javascriptSemanticQueryAssessment.js";
|
||||
import {
|
||||
canonicalJavaScriptSemanticQueryJson as canonicalJson,
|
||||
createJavaScriptSemanticQueryCursor as createCursor,
|
||||
JavaScriptSemanticQueryCursorError,
|
||||
javaScriptSemanticQueryIdentifier as queryIdentifier,
|
||||
parseJavaScriptSemanticQueryCursor as parseCursor,
|
||||
resolveJavaScriptSemanticQuerySeeds as resolveSeeds,
|
||||
} from "./javascriptSemanticQueryIdentity.js";
|
||||
|
||||
const TRAVERSAL_LIMITS = {
|
||||
max_seed_matches: 1_000,
|
||||
max_nodes: 50_000,
|
||||
max_edges: 100_000,
|
||||
max_depth: 64,
|
||||
max_functions: 10_000,
|
||||
max_modules: 10_000,
|
||||
max_unknowns: 10_000,
|
||||
} as const;
|
||||
|
||||
interface TraversalEntry {
|
||||
readonly relation: JavaScriptSemanticGraphRelation;
|
||||
readonly nextNodeId: string;
|
||||
@@ -45,21 +52,9 @@ interface SeedAdmission {
|
||||
readonly frontier: QueryFrontier[];
|
||||
}
|
||||
|
||||
interface QueryPage {
|
||||
interface QueryResult {
|
||||
readonly retainedNodes: JavaScriptSemanticGraphNode[];
|
||||
readonly retainedRelations: JavaScriptSemanticGraphRelation[];
|
||||
readonly pageRelations: JavaScriptSemanticGraphRelation[];
|
||||
readonly pageNodes: JavaScriptSemanticGraphNode[];
|
||||
readonly nextCursor: string | null;
|
||||
}
|
||||
|
||||
interface QueryPageInput {
|
||||
readonly graph: JavaScriptSemanticGraph;
|
||||
readonly traversal: Traversal;
|
||||
readonly query: JavaScriptSemanticQueryInput;
|
||||
readonly queryId: string;
|
||||
readonly offset: number;
|
||||
readonly retainedSeeds: readonly string[];
|
||||
}
|
||||
|
||||
/** Run one deterministic bounded traversal over a verified semantic graph. */
|
||||
@@ -69,7 +64,6 @@ export const queryJavaScriptSemanticGraph = (
|
||||
): JavaScriptSemanticQueryResult => {
|
||||
const input = javaScriptSemanticQueryInputSchema.parse(rawInput);
|
||||
const queryId = queryIdentifier(graph, input);
|
||||
const offset = parseCursor(input.cursor, queryId);
|
||||
const seeds = resolveSeeds(graph, input);
|
||||
const admission = admitSeeds(graph, seeds, input);
|
||||
const retainedSeeds = admission.nodeIds;
|
||||
@@ -79,14 +73,7 @@ export const queryJavaScriptSemanticGraph = (
|
||||
...traversal.frontier,
|
||||
...admission.frontier,
|
||||
]);
|
||||
const page = createQueryPage({
|
||||
graph,
|
||||
traversal,
|
||||
query: input,
|
||||
queryId,
|
||||
offset,
|
||||
retainedSeeds,
|
||||
});
|
||||
const result = createQueryResult(graph, traversal);
|
||||
const allRelevantUnknowns = relevantUnknownFrontiers(
|
||||
graph,
|
||||
traversal.nodeIds,
|
||||
@@ -94,7 +81,7 @@ export const queryJavaScriptSemanticGraph = (
|
||||
);
|
||||
const relevantUnknowns = allRelevantUnknowns.slice(
|
||||
0,
|
||||
input.limits.max_unknowns,
|
||||
TRAVERSAL_LIMITS.max_unknowns,
|
||||
);
|
||||
const candidateRelations = relevantCandidateRelationCount(
|
||||
graph,
|
||||
@@ -102,8 +89,8 @@ export const queryJavaScriptSemanticGraph = (
|
||||
input,
|
||||
);
|
||||
const expectedMatches = expectedMatchesFor(
|
||||
page.retainedNodes,
|
||||
page.retainedRelations,
|
||||
result.retainedNodes,
|
||||
result.retainedRelations,
|
||||
input,
|
||||
);
|
||||
const assessment = assessJavaScriptSemanticQuery({
|
||||
@@ -124,8 +111,8 @@ export const queryJavaScriptSemanticGraph = (
|
||||
direction: input.direction,
|
||||
status: assessment.status,
|
||||
seed_node_ids: retainedSeeds,
|
||||
nodes: page.pageNodes,
|
||||
relations: page.pageRelations,
|
||||
nodes: result.retainedNodes,
|
||||
relations: result.retainedRelations,
|
||||
unknowns: relevantUnknowns,
|
||||
expected_match_node_ids: expectedMatches.map(({ node_id }) => node_id),
|
||||
summary: {
|
||||
@@ -139,60 +126,21 @@ export const queryJavaScriptSemanticGraph = (
|
||||
retained_unknowns: relevantUnknowns.length,
|
||||
},
|
||||
coverage: assessment.coverage,
|
||||
page: {
|
||||
offset,
|
||||
size: page.pageRelations.length,
|
||||
next_cursor: page.nextCursor,
|
||||
},
|
||||
applied_limits: input.limits,
|
||||
accepted_limit_ranges: assessment.acceptedLimitRanges,
|
||||
limitations: assessment.limitations,
|
||||
});
|
||||
};
|
||||
|
||||
const createQueryPage = ({
|
||||
graph,
|
||||
traversal,
|
||||
query,
|
||||
queryId,
|
||||
offset,
|
||||
retainedSeeds,
|
||||
}: QueryPageInput): QueryPage => {
|
||||
const createQueryResult = (
|
||||
graph: JavaScriptSemanticGraph,
|
||||
traversal: Traversal,
|
||||
): QueryResult => {
|
||||
const retainedNodes = graph.nodes.filter(({ node_id }) =>
|
||||
traversal.nodeIds.has(node_id),
|
||||
);
|
||||
const retainedRelations = graph.relations.filter(({ relation_id }) =>
|
||||
traversal.relationIds.has(relation_id),
|
||||
);
|
||||
if (offset > retainedRelations.length)
|
||||
throw new JavaScriptSemanticQueryCursorError(
|
||||
"Semantic query cursor is beyond the retained result",
|
||||
);
|
||||
const pageRelations = retainedRelations.slice(
|
||||
offset,
|
||||
offset + query.limits.page_size,
|
||||
);
|
||||
const pageNodeIds = new Set([
|
||||
...retainedSeeds,
|
||||
...pageRelations.flatMap(({ source_node_id, target_node_id }) => [
|
||||
source_node_id,
|
||||
target_node_id,
|
||||
]),
|
||||
]);
|
||||
const pageNodes = graph.nodes.filter(({ node_id }) =>
|
||||
pageNodeIds.has(node_id),
|
||||
);
|
||||
const nextOffset = offset + pageRelations.length;
|
||||
return {
|
||||
retainedNodes,
|
||||
retainedRelations,
|
||||
pageRelations,
|
||||
pageNodes,
|
||||
nextCursor:
|
||||
nextOffset < retainedRelations.length
|
||||
? createCursor(queryId, nextOffset)
|
||||
: null,
|
||||
};
|
||||
return { retainedNodes, retainedRelations };
|
||||
};
|
||||
|
||||
const admitSeeds = (
|
||||
@@ -244,20 +192,19 @@ const seedBlockedReason = (
|
||||
node: JavaScriptSemanticGraphNode,
|
||||
bounds: SeedBounds,
|
||||
): QueryFrontier["reason"] | null => {
|
||||
if (bounds.retained.length >= bounds.input.limits.max_seed_matches)
|
||||
if (bounds.retained.length >= TRAVERSAL_LIMITS.max_seed_matches)
|
||||
return "max-seed-matches";
|
||||
if (bounds.retained.length >= bounds.input.limits.max_nodes)
|
||||
return "max-nodes";
|
||||
if (bounds.retained.length >= TRAVERSAL_LIMITS.max_nodes) return "max-nodes";
|
||||
const functionId = functionOwner(node);
|
||||
if (
|
||||
functionId !== null &&
|
||||
!bounds.functions.has(functionId) &&
|
||||
bounds.functions.size >= bounds.input.limits.max_functions
|
||||
bounds.functions.size >= TRAVERSAL_LIMITS.max_functions
|
||||
)
|
||||
return "max-functions";
|
||||
if (
|
||||
!bounds.modules.has(node.identity.module_path) &&
|
||||
bounds.modules.size >= bounds.input.limits.max_modules
|
||||
bounds.modules.size >= TRAVERSAL_LIMITS.max_modules
|
||||
)
|
||||
return "max-modules";
|
||||
return null;
|
||||
@@ -407,26 +354,26 @@ const blockedReason = (
|
||||
depth: number,
|
||||
bounds: TraversalBounds,
|
||||
): QueryFrontier["reason"] | null => {
|
||||
if (depth >= bounds.input.limits.max_depth) return "max-depth";
|
||||
if (depth >= TRAVERSAL_LIMITS.max_depth) return "max-depth";
|
||||
if (
|
||||
!bounds.relationIds.has(entry.relation.relation_id) &&
|
||||
bounds.relationIds.size >= bounds.input.limits.max_edges
|
||||
bounds.relationIds.size >= TRAVERSAL_LIMITS.max_edges
|
||||
)
|
||||
return "max-edges";
|
||||
if (bounds.nodeIds.has(entry.nextNodeId)) return null;
|
||||
if (bounds.nodeIds.size >= bounds.input.limits.max_nodes) return "max-nodes";
|
||||
if (bounds.nodeIds.size >= TRAVERSAL_LIMITS.max_nodes) return "max-nodes";
|
||||
const node = bounds.nodes.get(entry.nextNodeId);
|
||||
if (node === undefined) return null;
|
||||
const functionId = functionOwner(node);
|
||||
if (
|
||||
functionId !== null &&
|
||||
!bounds.functionIds.has(functionId) &&
|
||||
bounds.functionIds.size >= bounds.input.limits.max_functions
|
||||
bounds.functionIds.size >= TRAVERSAL_LIMITS.max_functions
|
||||
)
|
||||
return "max-functions";
|
||||
if (
|
||||
!bounds.modules.has(node.identity.module_path) &&
|
||||
bounds.modules.size >= bounds.input.limits.max_modules
|
||||
bounds.modules.size >= TRAVERSAL_LIMITS.max_modules
|
||||
)
|
||||
return "max-modules";
|
||||
return null;
|
||||
|
||||
@@ -34,7 +34,6 @@ export interface JavaScriptSemanticQueryAssessment {
|
||||
readonly status: JavaScriptSemanticQueryResult["status"];
|
||||
readonly coverage: JavaScriptSemanticQueryResult["coverage"];
|
||||
readonly limitations: string[];
|
||||
readonly acceptedLimitRanges: JavaScriptSemanticQueryResult["accepted_limit_ranges"];
|
||||
}
|
||||
|
||||
/** Classify a bounded semantic traversal without promoting unknowns to absence. */
|
||||
@@ -44,16 +43,6 @@ export const assessJavaScriptSemanticQuery = (
|
||||
status: queryStatus(input),
|
||||
coverage: queryCoverage(input),
|
||||
limitations: queryLimitations(input),
|
||||
acceptedLimitRanges: {
|
||||
max_seed_matches: { minimum: 1, maximum: 1_000 },
|
||||
max_nodes: { minimum: 1, maximum: 50_000 },
|
||||
max_edges: { minimum: 1, maximum: 100_000 },
|
||||
max_depth: { minimum: 0, maximum: 64 },
|
||||
max_functions: { minimum: 1, maximum: 10_000 },
|
||||
max_modules: { minimum: 1, maximum: 10_000 },
|
||||
max_unknowns: { minimum: 0, maximum: 10_000 },
|
||||
page_size: { minimum: 1, maximum: 1_000 },
|
||||
},
|
||||
});
|
||||
|
||||
const queryCoverage = (
|
||||
@@ -98,12 +87,12 @@ const queryLimitations = (
|
||||
...(input.totalSeeds === input.retainedSeeds
|
||||
? []
|
||||
: [
|
||||
"Seed matches exceeded caller bounds; omitted starts remain unexplored.",
|
||||
"Seed matches exceeded the internal traversal safety ceiling; omitted starts remain unexplored.",
|
||||
]),
|
||||
...(input.frontier.length === 0
|
||||
? []
|
||||
: [
|
||||
"Traversal stopped at explicit caller limits; frontier facts remain unknown.",
|
||||
"Traversal stopped at an internal resource ceiling; frontier facts remain unknown.",
|
||||
]),
|
||||
...(input.unknowns.length === 0
|
||||
? []
|
||||
@@ -112,7 +101,7 @@ const queryLimitations = (
|
||||
]),
|
||||
...(input.unknownsTruncated
|
||||
? [
|
||||
"Relevant unknown frontiers exceeded the caller limit; omitted unknowns remain unresolved.",
|
||||
"Relevant unknown frontiers exceeded the internal result safety ceiling; omitted unknowns remain unresolved.",
|
||||
]
|
||||
: []),
|
||||
...(input.candidateRelations === 0
|
||||
|
||||
@@ -9,9 +9,6 @@ import type {
|
||||
} from "./javascriptSemanticGraph.js";
|
||||
import type { JavaScriptSemanticQueryInput } from "./javascriptSemanticQuerySchemas.js";
|
||||
|
||||
/** Caller cursor that cannot identify a page of the current semantic query. */
|
||||
export class JavaScriptSemanticQueryCursorError extends TypeError {}
|
||||
|
||||
/** Canonical JSON used by semantic query commitments and ordering. */
|
||||
export const canonicalJavaScriptSemanticQueryJson = (
|
||||
value: unknown,
|
||||
@@ -32,12 +29,12 @@ const digest = (value: unknown): string =>
|
||||
const uniqueSorted = (values: readonly string[]): string[] =>
|
||||
[...new Set(values)].sort(compareCodePoints);
|
||||
|
||||
/** Derive the stable identity for one graph-bound query without its cursor. */
|
||||
/** Derive the stable identity for one graph-bound query. */
|
||||
export const javaScriptSemanticQueryIdentifier = (
|
||||
graph: JavaScriptSemanticGraph,
|
||||
input: JavaScriptSemanticQueryInput,
|
||||
): string => {
|
||||
const { cursor: _cursor, ...semantic } = input;
|
||||
const semantic = input;
|
||||
return `jsrq_${digest({
|
||||
source_graph_id: graph.graph_id,
|
||||
...semantic,
|
||||
@@ -55,37 +52,6 @@ export const javaScriptSemanticQueryIdentifier = (
|
||||
})}`;
|
||||
};
|
||||
|
||||
/** Commit one deterministic relation-page offset to a query identity. */
|
||||
export const createJavaScriptSemanticQueryCursor = (
|
||||
queryId: string,
|
||||
offset: number,
|
||||
): string => `jsrqc_${String(offset)}_${digest({ query_id: queryId, offset })}`;
|
||||
|
||||
/** Parse a query-bound cursor and reject stale, malformed, or foreign values. */
|
||||
export const parseJavaScriptSemanticQueryCursor = (
|
||||
cursor: string | null,
|
||||
queryId: string,
|
||||
): number => {
|
||||
if (cursor === null) return 0;
|
||||
const match = /^jsrqc_([0-9]+)_([a-f0-9]{64})$/u.exec(cursor);
|
||||
const offsetText = match?.[1];
|
||||
const commitment = match?.[2];
|
||||
if (offsetText === undefined || commitment === undefined)
|
||||
throw new JavaScriptSemanticQueryCursorError(
|
||||
"Semantic query cursor is malformed",
|
||||
);
|
||||
const offset = Number(offsetText);
|
||||
if (
|
||||
!Number.isSafeInteger(offset) ||
|
||||
offset < 0 ||
|
||||
commitment !== digest({ query_id: queryId, offset })
|
||||
)
|
||||
throw new JavaScriptSemanticQueryCursorError(
|
||||
"Semantic query cursor does not match this query",
|
||||
);
|
||||
return offset;
|
||||
};
|
||||
|
||||
/** Resolve deterministic seed candidates without applying traversal limits. */
|
||||
export const resolveJavaScriptSemanticQuerySeeds = (
|
||||
graph: JavaScriptSemanticGraph,
|
||||
|
||||
@@ -57,18 +57,6 @@ export const javaScriptSemanticQuerySeedSchema = z.discriminatedUnion("kind", [
|
||||
}),
|
||||
]);
|
||||
|
||||
/** Strict caller limits for deterministic bounded traversal. */
|
||||
export const javaScriptSemanticQueryLimitsSchema = z.strictObject({
|
||||
max_seed_matches: z.number().int().min(1).max(1_000).default(25),
|
||||
max_nodes: z.number().int().min(1).max(50_000).default(2_000),
|
||||
max_edges: z.number().int().min(1).max(100_000).default(4_000),
|
||||
max_depth: z.number().int().min(0).max(64).default(12),
|
||||
max_functions: z.number().int().min(1).max(10_000).default(500),
|
||||
max_modules: z.number().int().min(1).max(10_000).default(500),
|
||||
max_unknowns: z.number().int().min(0).max(10_000).default(1_000),
|
||||
page_size: z.number().int().min(1).max(1_000).default(200),
|
||||
});
|
||||
|
||||
const sourceMapAuthoritySchema = z.strictObject({
|
||||
authority: z.literal("none"),
|
||||
});
|
||||
@@ -96,21 +84,6 @@ export const javaScriptSemanticQueryInputSchema = z.strictObject({
|
||||
.nullable()
|
||||
.default(null),
|
||||
source_map_authority: sourceMapAuthoritySchema.default({ authority: "none" }),
|
||||
limits: javaScriptSemanticQueryLimitsSchema.default({
|
||||
max_seed_matches: 25,
|
||||
max_nodes: 2_000,
|
||||
max_edges: 4_000,
|
||||
max_depth: 12,
|
||||
max_functions: 500,
|
||||
max_modules: 500,
|
||||
max_unknowns: 1_000,
|
||||
page_size: 200,
|
||||
}),
|
||||
cursor: z
|
||||
.string()
|
||||
.regex(/^jsrqc_[0-9]+_[a-f0-9]{64}$/u)
|
||||
.nullable()
|
||||
.default(null),
|
||||
});
|
||||
|
||||
const queryFrontierSchema = z.strictObject({
|
||||
@@ -126,7 +99,7 @@ const queryFrontierSchema = z.strictObject({
|
||||
]),
|
||||
});
|
||||
|
||||
/** Deterministic, pageable semantic trace result. */
|
||||
/** Deterministic semantic trace result. */
|
||||
export const javaScriptSemanticQueryResultSchema = z.strictObject({
|
||||
query_id: z.string().regex(/^jsrq_[a-f0-9]{64}$/u),
|
||||
source_graph_id: z.string().regex(/^jsrg_[a-f0-9]{64}$/u),
|
||||
@@ -159,49 +132,6 @@ export const javaScriptSemanticQueryResultSchema = z.strictObject({
|
||||
status: z.enum(["complete", "partial", "truncated", "unavailable"]),
|
||||
frontier: z.array(queryFrontierSchema).max(100_000),
|
||||
}),
|
||||
page: z.strictObject({
|
||||
offset: z.number().int().min(0),
|
||||
size: z.number().int().min(0).max(1_000),
|
||||
next_cursor: z
|
||||
.string()
|
||||
.regex(/^jsrqc_[0-9]+_[a-f0-9]{64}$/u)
|
||||
.nullable(),
|
||||
}),
|
||||
applied_limits: javaScriptSemanticQueryLimitsSchema,
|
||||
accepted_limit_ranges: z.strictObject({
|
||||
max_seed_matches: z.strictObject({
|
||||
minimum: z.literal(1),
|
||||
maximum: z.literal(1_000),
|
||||
}),
|
||||
max_nodes: z.strictObject({
|
||||
minimum: z.literal(1),
|
||||
maximum: z.literal(50_000),
|
||||
}),
|
||||
max_edges: z.strictObject({
|
||||
minimum: z.literal(1),
|
||||
maximum: z.literal(100_000),
|
||||
}),
|
||||
max_depth: z.strictObject({
|
||||
minimum: z.literal(0),
|
||||
maximum: z.literal(64),
|
||||
}),
|
||||
max_functions: z.strictObject({
|
||||
minimum: z.literal(1),
|
||||
maximum: z.literal(10_000),
|
||||
}),
|
||||
max_modules: z.strictObject({
|
||||
minimum: z.literal(1),
|
||||
maximum: z.literal(10_000),
|
||||
}),
|
||||
max_unknowns: z.strictObject({
|
||||
minimum: z.literal(0),
|
||||
maximum: z.literal(10_000),
|
||||
}),
|
||||
page_size: z.strictObject({
|
||||
minimum: z.literal(1),
|
||||
maximum: z.literal(1_000),
|
||||
}),
|
||||
}),
|
||||
limitations: z.array(z.string().min(1).max(4_096)).max(1_000),
|
||||
});
|
||||
|
||||
|
||||
@@ -283,22 +283,6 @@ export const reconstructionObligationLedgerInputSchema = z.strictObject({
|
||||
evidence_bundle: evidenceBundleSchema,
|
||||
reviewed_obligations: z.array(reviewedReconstructionObligationSchema),
|
||||
manifest: reconstructionObligationManifestSchema,
|
||||
page: z.strictObject({
|
||||
offset: z.number().int().min(0).max(10_000),
|
||||
limit: z.number().int().min(1).max(100),
|
||||
}),
|
||||
});
|
||||
|
||||
export const reconstructionObligationLedgerPageSchema = z.strictObject({
|
||||
...reconstructionObligationLedgerSchema.omit({ obligations: true }).shape,
|
||||
page: z.strictObject({
|
||||
offset: z.number().int().min(0),
|
||||
limit: z.number().int().min(1).max(100),
|
||||
total: z.number().int().min(0),
|
||||
returned: z.number().int().min(0),
|
||||
next_offset: z.number().int().min(0).nullable(),
|
||||
}),
|
||||
obligations: z.array(reconstructionObligationSchema).max(100),
|
||||
});
|
||||
|
||||
export type ReconstructionObligation = z.infer<
|
||||
@@ -310,9 +294,6 @@ export type ReconstructionObligationLedger = z.infer<
|
||||
export type ReconstructionObligationLedgerInput = z.infer<
|
||||
typeof reconstructionObligationLedgerInputSchema
|
||||
>;
|
||||
export type ReconstructionObligationLedgerPage = z.infer<
|
||||
typeof reconstructionObligationLedgerPageSchema
|
||||
>;
|
||||
export type ReviewedReconstructionObligation = z.infer<
|
||||
typeof reviewedReconstructionObligationSchema
|
||||
>;
|
||||
|
||||
@@ -66,14 +66,11 @@ export const reconstructionClaimUnknowns = (
|
||||
);
|
||||
};
|
||||
|
||||
/** Aggregate and bound deterministic probes for unresolved claims. */
|
||||
/** Aggregate deterministic probes for unresolved claims. */
|
||||
export const reconstructionProbes = (
|
||||
results: readonly ReconstructionClaimResult[],
|
||||
heads: ReadonlyMap<string, ResidualUnknown>,
|
||||
): {
|
||||
readonly items: ReconstructionVerificationResult["recommended_probes"];
|
||||
readonly truncated: boolean;
|
||||
} => {
|
||||
): ReconstructionVerificationResult["recommended_probes"] => {
|
||||
const output = new Map<
|
||||
string,
|
||||
{
|
||||
@@ -111,7 +108,7 @@ export const reconstructionProbes = (
|
||||
"en",
|
||||
),
|
||||
);
|
||||
return { items: sorted.slice(0, 2_000), truncated: sorted.length > 2_000 };
|
||||
return sorted;
|
||||
};
|
||||
|
||||
const defaultProbe = (
|
||||
|
||||
@@ -118,19 +118,35 @@ const behavioralSpec = (comparison: Evidence) => ({
|
||||
],
|
||||
});
|
||||
|
||||
const artifactResult = (partial = false): JsonValue => ({
|
||||
status: "unchanged",
|
||||
const artifactResult = (
|
||||
partial = false,
|
||||
evidenceLinks: readonly string[] = [],
|
||||
): JsonValue => ({
|
||||
status: partial ? "truncated" : "unchanged",
|
||||
left_manifest_id: `agm_${"3".repeat(64)}`,
|
||||
right_manifest_id: `agm_${"4".repeat(64)}`,
|
||||
summary: { unchanged: 1, added: 0, removed: 0, changed: 0, unknown: 0 },
|
||||
changes: {
|
||||
items: [],
|
||||
offset: 0,
|
||||
limit: 100,
|
||||
total: partial ? 1 : 0,
|
||||
next_offset: partial ? 1 : null,
|
||||
summary: {
|
||||
unchanged: partial ? 0 : 1,
|
||||
added: 0,
|
||||
removed: 0,
|
||||
changed: 0,
|
||||
unknown: partial ? 1 : 0,
|
||||
},
|
||||
limitations: [],
|
||||
changes: partial
|
||||
? [
|
||||
{
|
||||
classification: "unknown",
|
||||
logical_path: "unavailable",
|
||||
dimensions: ["availability"],
|
||||
left_occurrence_id: null,
|
||||
right_occurrence_id: null,
|
||||
left_artifact_id: null,
|
||||
right_artifact_id: null,
|
||||
evidence_links: [...evidenceLinks],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
limitations: partial ? ["Inventory evidence is incomplete."] : [],
|
||||
});
|
||||
|
||||
const artifactComparison = (
|
||||
@@ -152,7 +168,7 @@ const artifactComparison = (
|
||||
left_evidence_ids: [left.evidence_id],
|
||||
right_evidence_ids: [right.evidence_id],
|
||||
},
|
||||
result: artifactResult(partial),
|
||||
result: artifactResult(partial, [left.evidence_id, right.evidence_id]),
|
||||
confidence: "derived",
|
||||
authority: "analyst-inference",
|
||||
evidenceLinks: [left.evidence_id, right.evidence_id],
|
||||
@@ -207,8 +223,6 @@ describe("reconstruction verification", () => {
|
||||
],
|
||||
},
|
||||
createEvidenceBundle([comparison, right, left]),
|
||||
0,
|
||||
100,
|
||||
),
|
||||
).toMatchObject({
|
||||
summary: { total: 2, passed: 1, failed: 1, unknown: 0 },
|
||||
@@ -222,13 +236,11 @@ describe("reconstruction verification", () => {
|
||||
const result = verifyReconstruction(
|
||||
behavioralSpec(comparison),
|
||||
createEvidenceBundle([comparison, right, left]),
|
||||
0,
|
||||
100,
|
||||
);
|
||||
expect(reconstructionVerificationResultSchema.parse(result)).toMatchObject({
|
||||
status: "pass",
|
||||
summary: { total: 1, passed: 1, failed: 0, unknown: 0 },
|
||||
claims: { total: 1, next_offset: null },
|
||||
claims: { items: expect.any(Array) },
|
||||
});
|
||||
expect(
|
||||
reconstructionVerificationResultSchema.safeParse({
|
||||
@@ -258,8 +270,6 @@ describe("reconstruction verification", () => {
|
||||
const result = verifyReconstruction(
|
||||
behavioralSpec(comparison),
|
||||
createEvidenceBundle([left, right, comparison]),
|
||||
0,
|
||||
100,
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
status: "fail",
|
||||
@@ -276,8 +286,6 @@ describe("reconstruction verification", () => {
|
||||
verifyReconstruction(
|
||||
behavioralSpec(runtime),
|
||||
createEvidenceBundle([weakLeft, replayRight, runtime]),
|
||||
0,
|
||||
100,
|
||||
).status,
|
||||
).toBe("unknown");
|
||||
|
||||
@@ -298,8 +306,6 @@ describe("reconstruction verification", () => {
|
||||
],
|
||||
},
|
||||
createEvidenceBundle([left, comparison, right]),
|
||||
0,
|
||||
100,
|
||||
);
|
||||
expect(result.status).toBe("unknown");
|
||||
expect(result.recommended_probes[0]?.operation).toBe("inventory_artifact");
|
||||
@@ -318,13 +324,11 @@ describe("reconstruction verification integrity", () => {
|
||||
verifyReconstruction(
|
||||
behavioralSpec(comparison),
|
||||
createEvidenceBundle([left, right, extra, comparison]),
|
||||
0,
|
||||
100,
|
||||
),
|
||||
).toThrow(/closure disagrees/u);
|
||||
});
|
||||
|
||||
it("pages claims deterministically and gives specification a stable digest", () => {
|
||||
it("returns every claim deterministically and gives specification a stable digest", () => {
|
||||
const left = source("1", "controlled-replay");
|
||||
const right = source("2", "controlled-replay");
|
||||
const comparison = processComparison(left, right, processResult());
|
||||
@@ -342,8 +346,6 @@ describe("reconstruction verification integrity", () => {
|
||||
const first = verifyReconstruction(
|
||||
{ name: "Compatibility", claims },
|
||||
bundle,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
const repeated = verifyReconstruction(
|
||||
{
|
||||
@@ -351,15 +353,15 @@ describe("reconstruction verification integrity", () => {
|
||||
claims: [...claims].reverse(),
|
||||
},
|
||||
bundle,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
expect(first).toEqual(repeated);
|
||||
expect(first.claims.items[0]?.claim_id).toBe("terminal-output");
|
||||
expect(first.claims.next_offset).toBe(1);
|
||||
expect(first.claims.items.map(({ claim_id }) => claim_id)).toEqual([
|
||||
"terminal-output",
|
||||
"z-exit",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps all active unknowns gating while bounding their projection", () => {
|
||||
it("returns all active unknowns while keeping them gating", () => {
|
||||
const left = source("1", "controlled-replay");
|
||||
const right = source("2", "controlled-replay");
|
||||
const comparison = processComparison(left, right, processResult());
|
||||
@@ -400,13 +402,8 @@ describe("reconstruction verification integrity", () => {
|
||||
const result = verifyReconstruction(
|
||||
behavioralSpec(comparison),
|
||||
createEvidenceBundle([left, right, comparison, ...mutations], unknowns),
|
||||
0,
|
||||
100,
|
||||
);
|
||||
expect(result.status).toBe("unknown");
|
||||
expect(result.claims.items[0]?.unknown_ids).toHaveLength(100);
|
||||
expect(result.claims.items[0]?.limitations).toContain(
|
||||
"1 additional active residual unknowns affect this claim but are omitted by the 100-item display limit.",
|
||||
);
|
||||
expect(result.claims.items[0]?.unknown_ids).toHaveLength(101);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,8 +73,6 @@ const acceptsPredicate = (
|
||||
export const verifyReconstruction = (
|
||||
specificationInput: unknown,
|
||||
bundleInput: unknown,
|
||||
offset: number,
|
||||
limit: number,
|
||||
): ReconstructionVerificationResult => {
|
||||
const specification =
|
||||
reconstructionSpecificationSchema.parse(specificationInput);
|
||||
@@ -91,7 +89,6 @@ export const verifyReconstruction = (
|
||||
const results = specification.claims
|
||||
.map((claim) => evaluateClaim(claim, records, heads))
|
||||
.sort((left, right) => left.claim_id.localeCompare(right.claim_id, "en"));
|
||||
const page = results.slice(offset, offset + limit);
|
||||
const failed = count(results, "fail");
|
||||
const unresolved = count(results, "unknown");
|
||||
const probes = reconstructionProbes(results, heads);
|
||||
@@ -117,20 +114,12 @@ export const verifyReconstruction = (
|
||||
structural: results.filter(({ kind }) => kind !== "behavioral").length,
|
||||
},
|
||||
claims: {
|
||||
items: page,
|
||||
offset,
|
||||
limit,
|
||||
total: results.length,
|
||||
next_offset:
|
||||
offset + page.length < results.length ? offset + page.length : null,
|
||||
items: results,
|
||||
},
|
||||
recommended_probes: probes.items,
|
||||
recommended_probes: probes,
|
||||
evidence_links: evidenceLinks,
|
||||
limitations: [
|
||||
"Pass means every declared claim passed; it does not establish global implementation equivalence.",
|
||||
...(probes.truncated
|
||||
? ["Recommended probes were truncated at the 2,000-item limit."]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
return reconstructionVerificationResultSchema.parse(output);
|
||||
@@ -172,8 +161,7 @@ const evaluateClaim = (
|
||||
allSourceIds,
|
||||
);
|
||||
const status = classify(observed, limitations, relevantUnknowns);
|
||||
const displayedUnknowns = relevantUnknowns.slice(0, 100);
|
||||
const hiddenUnknownCount = relevantUnknowns.length - displayedUnknowns.length;
|
||||
const displayedUnknowns = relevantUnknowns;
|
||||
return reconstructionClaimResultSchema.parse({
|
||||
claim_id: claim.claim_id,
|
||||
kind: claim.kind,
|
||||
@@ -191,11 +179,6 @@ const evaluateClaim = (
|
||||
...displayedUnknowns.map(
|
||||
({ question }) => `Residual unknown: ${question}`,
|
||||
),
|
||||
...(hiddenUnknownCount > 0
|
||||
? [
|
||||
`${hiddenUnknownCount} additional active residual unknowns affect this claim but are omitted by the 100-item display limit.`,
|
||||
]
|
||||
: []),
|
||||
]),
|
||||
});
|
||||
};
|
||||
@@ -307,13 +290,7 @@ const observedStatus = (claim: Claim, evidence: Evidence): ObservedStatus => {
|
||||
evidence.normalized_result,
|
||||
);
|
||||
if (
|
||||
result.changes.offset !== 0 ||
|
||||
result.changes.next_offset !== null ||
|
||||
result.changes.items.length !== result.changes.total
|
||||
)
|
||||
return "unknown";
|
||||
if (
|
||||
result.changes.items.some((item) =>
|
||||
result.changes.some((item) =>
|
||||
item.evidence_links.some((id) => !evidence.evidence_links.includes(id)),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@ const reconstructionClaimSchema = z.discriminatedUnion("kind", [
|
||||
export const reconstructionSpecificationSchema = z
|
||||
.object({
|
||||
name: z.string().trim().min(1).max(200),
|
||||
claims: z.array(reconstructionClaimSchema).min(1).max(100),
|
||||
claims: z.array(reconstructionClaimSchema).min(1),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
const ids = value.claims.map(({ claim_id: id }) => id);
|
||||
@@ -85,8 +85,6 @@ export const reconstructionVerificationInputSchema = z.strictObject({
|
||||
boundary_id: z.string().min(1).max(200),
|
||||
})
|
||||
.optional(),
|
||||
offset: z.number().int().min(0).default(0),
|
||||
limit: z.number().int().min(1).max(100).default(100),
|
||||
unknown_registry_approved: z.literal(true).optional(),
|
||||
});
|
||||
|
||||
@@ -105,20 +103,20 @@ export const reconstructionClaimResultSchema = z.object({
|
||||
"contradiction",
|
||||
]),
|
||||
comparison_evidence_id: evidenceIdSchema,
|
||||
left_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
|
||||
right_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
|
||||
left_evidence_ids: z.array(evidenceIdSchema).min(1),
|
||||
right_evidence_ids: z.array(evidenceIdSchema).min(1),
|
||||
evidence_links: z.array(evidenceIdSchema).min(3).max(201),
|
||||
unknown_ids: z.array(unknownIdSchema).max(100),
|
||||
unknown_ids: z.array(unknownIdSchema),
|
||||
limitations: z.array(z.string()),
|
||||
});
|
||||
|
||||
/** Evidence-cited result over every declared claim, independently paged. */
|
||||
/** Evidence-cited result over every declared claim. */
|
||||
export const reconstructionVerificationResultSchema = z
|
||||
.object({
|
||||
status: verificationStatusSchema,
|
||||
specification_sha256: digestSchema,
|
||||
summary: z.object({
|
||||
total: z.number().int().min(1).max(100),
|
||||
total: z.number().int().min(1),
|
||||
passed: z.number().int().min(0),
|
||||
failed: z.number().int().min(0),
|
||||
unknown: z.number().int().min(0),
|
||||
@@ -126,22 +124,16 @@ export const reconstructionVerificationResultSchema = z
|
||||
structural: z.number().int().min(0),
|
||||
}),
|
||||
claims: z.object({
|
||||
items: z.array(reconstructionClaimResultSchema).max(100),
|
||||
offset: z.number().int().min(0),
|
||||
limit: z.number().int().min(1).max(100),
|
||||
total: z.number().int().min(1).max(100),
|
||||
next_offset: z.number().int().min(0).nullable(),
|
||||
items: z.array(reconstructionClaimResultSchema),
|
||||
}),
|
||||
recommended_probes: z
|
||||
.array(
|
||||
z.object({
|
||||
operation: z.string().min(1).max(200),
|
||||
rationale: z.string().min(1).max(1_000),
|
||||
claim_ids: z.array(claimIdSchema).min(1).max(100),
|
||||
unknown_ids: z.array(unknownIdSchema).max(100),
|
||||
}),
|
||||
)
|
||||
.max(2_000),
|
||||
recommended_probes: z.array(
|
||||
z.object({
|
||||
operation: z.string().min(1).max(200),
|
||||
rationale: z.string().min(1).max(1_000),
|
||||
claim_ids: z.array(claimIdSchema).min(1),
|
||||
unknown_ids: z.array(unknownIdSchema),
|
||||
}),
|
||||
),
|
||||
evidence_links: z.array(evidenceIdSchema).min(3).max(20_100),
|
||||
limitations: z.array(z.string()),
|
||||
})
|
||||
@@ -161,9 +153,9 @@ export const reconstructionVerificationResultSchema = z
|
||||
["summary", "total"],
|
||||
],
|
||||
[
|
||||
result.claims.total !== summary.total,
|
||||
"Paged claim total must equal the summary total",
|
||||
["claims", "total"],
|
||||
result.claims.items.length !== summary.total,
|
||||
"Retained claims must equal the summary total",
|
||||
["claims", "items"],
|
||||
],
|
||||
[
|
||||
result.status !== expectedStatus,
|
||||
|
||||
@@ -10,7 +10,6 @@ const digest = (character: string): string => character.repeat(64);
|
||||
describe("provider-neutral runtime characterization plan", () => {
|
||||
it("commits preparation separately from execution approval", () => {
|
||||
const plan = createRuntimeCharacterizationPlan({
|
||||
schema_version: 1,
|
||||
preparation_sha256: digest("5"),
|
||||
artifact: {
|
||||
path: "/approved/app.js",
|
||||
|
||||
@@ -33,7 +33,6 @@ const callableIdentitySchema = z.strictObject({
|
||||
});
|
||||
|
||||
export const runtimeCharacterizationPlanSchema = z.strictObject({
|
||||
schema_version: z.literal(1),
|
||||
plan_sha256: digestSchema,
|
||||
preparation_sha256: digestSchema,
|
||||
artifact: artifactIdentitySchema,
|
||||
|
||||
@@ -132,7 +132,6 @@ describe("static/runtime correlation", () => {
|
||||
status: "correlated",
|
||||
summary: { hypotheses: 1, contradictions: 0, unresolved: 0 },
|
||||
correlations: {
|
||||
total: 1,
|
||||
items: [
|
||||
{
|
||||
observed_pattern: "cochanged",
|
||||
@@ -187,7 +186,7 @@ describe("static/runtime correlation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("is order-stable, paginated, and rejects duplicates and absent selectors", () => {
|
||||
it("returns all correlations order-stably and rejects duplicates and absent selectors", () => {
|
||||
const runtime = processComparison("changed");
|
||||
const base = mapping(runtime, "cochanged");
|
||||
const second = {
|
||||
@@ -201,17 +200,13 @@ describe("static/runtime correlation", () => {
|
||||
const forward = correlateStaticAndRuntime({
|
||||
...base,
|
||||
mappings: [base.mappings[0], second],
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
});
|
||||
const reversed = correlateStaticAndRuntime({
|
||||
...base,
|
||||
mappings: [second, base.mappings[0]],
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
});
|
||||
expect(reversed).toEqual(forward);
|
||||
expect(forward.correlations).toMatchObject({ total: 2, next_offset: 1 });
|
||||
expect(forward.correlations.items).toHaveLength(2);
|
||||
expect(() =>
|
||||
correlateStaticAndRuntime({
|
||||
...base,
|
||||
|
||||
@@ -88,8 +88,6 @@ export const staticRuntimeCorrelationInputSchema = z
|
||||
static_comparisons: z.array(evidenceSchema).min(1).max(100),
|
||||
runtime_comparisons: z.array(evidenceSchema).min(1).max(100),
|
||||
mappings: z.array(mappingSchema).min(1).max(500),
|
||||
offset: z.number().int().min(0).default(0),
|
||||
limit: z.number().int().min(1).max(100).default(100),
|
||||
unknown_registry_approved: z.literal(true).optional(),
|
||||
})
|
||||
.strict();
|
||||
@@ -122,7 +120,7 @@ const correlationItemSchema = z.object({
|
||||
limitations: z.array(z.string()),
|
||||
});
|
||||
|
||||
/** Deterministic, paginated static/runtime correlation payload. */
|
||||
/** Deterministic static/runtime correlation payload. */
|
||||
export const staticRuntimeCorrelationResultSchema = z.object({
|
||||
status: z.enum(["correlated", "contradicted", "unknown", "truncated"]),
|
||||
summary: z.object({
|
||||
@@ -131,11 +129,7 @@ export const staticRuntimeCorrelationResultSchema = z.object({
|
||||
unresolved: z.number().int().min(0),
|
||||
}),
|
||||
correlations: z.object({
|
||||
items: z.array(correlationItemSchema).max(100),
|
||||
offset: z.number().int().min(0),
|
||||
limit: z.number().int().min(1).max(100),
|
||||
total: z.number().int().min(0),
|
||||
next_offset: z.number().int().min(0).nullable(),
|
||||
items: z.array(correlationItemSchema),
|
||||
}),
|
||||
evidence_links: z.array(evidenceIdSchema).min(2).max(20_100),
|
||||
limitations: z.array(z.string()),
|
||||
@@ -191,7 +185,6 @@ export const correlateStaticAndRuntime = (
|
||||
throw new TypeError(
|
||||
"Static/runtime correlation rejects duplicate mappings",
|
||||
);
|
||||
const page = correlations.slice(parsed.offset, parsed.offset + parsed.limit);
|
||||
const links = uniqueSorted(
|
||||
correlations.flatMap(({ evidence_links: evidenceLinks }) => evidenceLinks),
|
||||
);
|
||||
@@ -220,14 +213,7 @@ export const correlateStaticAndRuntime = (
|
||||
unresolved: unresolved.length,
|
||||
},
|
||||
correlations: {
|
||||
items: page,
|
||||
offset: parsed.offset,
|
||||
limit: parsed.limit,
|
||||
total: correlations.length,
|
||||
next_offset:
|
||||
parsed.offset + page.length < correlations.length
|
||||
? parsed.offset + page.length
|
||||
: null,
|
||||
items: correlations,
|
||||
},
|
||||
evidence_links: links,
|
||||
limitations: [
|
||||
@@ -324,15 +310,7 @@ const selectStatic = (
|
||||
const result = artifactComparisonResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
if (
|
||||
result.changes.offset !== 0 ||
|
||||
result.changes.next_offset !== null ||
|
||||
result.changes.items.length !== result.changes.total
|
||||
)
|
||||
throw new TypeError(
|
||||
"Static/runtime correlation requires complete artifact comparison pagination",
|
||||
);
|
||||
const matches = result.changes.items.filter(
|
||||
const matches = result.changes.filter(
|
||||
({ logical_path: path }) => path === selector.logical_path,
|
||||
);
|
||||
const match = matches.at(0);
|
||||
@@ -366,10 +344,9 @@ const parseStatic = (input: unknown): Evidence => {
|
||||
const result = artifactComparisonResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
assertCompleteArtifactResult(result);
|
||||
assertNestedLinks(
|
||||
evidence,
|
||||
result.changes.items.flatMap(({ evidence_links: links }) => links),
|
||||
result.changes.flatMap(({ evidence_links: links }) => links),
|
||||
);
|
||||
} else {
|
||||
const result = functionComparisonResultSchema.parse(
|
||||
@@ -470,19 +447,6 @@ const sameSet = (left: readonly string[], right: readonly string[]): boolean =>
|
||||
new Set(left).size === left.length &&
|
||||
left.every((item) => right.includes(item));
|
||||
|
||||
const assertCompleteArtifactResult = (
|
||||
result: z.infer<typeof artifactComparisonResultSchema>,
|
||||
): void => {
|
||||
if (
|
||||
result.changes.offset !== 0 ||
|
||||
result.changes.next_offset !== null ||
|
||||
result.changes.items.length !== result.changes.total
|
||||
)
|
||||
throw new TypeError(
|
||||
"Static/runtime correlation requires complete artifact comparison pagination",
|
||||
);
|
||||
};
|
||||
|
||||
const observedPattern = (
|
||||
staticStatus: ComparisonStatus,
|
||||
runtimeStatus: ComparisonStatus,
|
||||
|
||||
@@ -369,7 +369,6 @@ export const buildNativeBoundaryInspection = ({
|
||||
issues,
|
||||
}: BoundaryInspectionContext): ManagedNativeBoundaryInspection =>
|
||||
managedNativeBoundaryInspectionSchema.parse({
|
||||
schema_version: 1,
|
||||
artifact: {
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
|
||||
@@ -89,12 +89,6 @@ export const MANAGED_MEMBER_FIXTURE_LIMITS = {
|
||||
maxMethodInstructions: 1_000,
|
||||
};
|
||||
|
||||
/** Member bounds for fixture tests that inspect a real local path. */
|
||||
export const MANAGED_MEMBER_PATH_FIXTURE_LIMITS = {
|
||||
...MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
maxFileBytes: 1024 * 1024,
|
||||
};
|
||||
|
||||
/** Generous native-boundary bounds for the source-owned PE fixture. */
|
||||
export const MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS = {
|
||||
maxMetadataBytes: 1024 * 1024,
|
||||
|
||||
@@ -50,8 +50,6 @@ export class ManagedStaticProvider implements AnalysisProvider {
|
||||
operation: contract.name,
|
||||
available: true as const,
|
||||
reason: null,
|
||||
pagination: "none" as const,
|
||||
exhaustive: true,
|
||||
effects: Object.freeze({
|
||||
mutatesArtifact: false,
|
||||
launchesProcess: false,
|
||||
@@ -61,11 +59,6 @@ export class ManagedStaticProvider implements AnalysisProvider {
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
}),
|
||||
limits: Object.freeze({
|
||||
maxResults: null,
|
||||
maxPayloadBytes: 4 * 1024 * 1024,
|
||||
timeoutMs: null,
|
||||
}),
|
||||
limitations: limitationsFor(contract.name),
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -95,7 +95,6 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
|
||||
await writeFileAtomic(
|
||||
paths.descriptorPath,
|
||||
`${JSON.stringify({
|
||||
schema_version: 2,
|
||||
transport: session.transport,
|
||||
endpoint_path: session.endpointPath,
|
||||
token: session.token,
|
||||
@@ -137,7 +136,6 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
|
||||
await writeFileAtomic(
|
||||
paths.ownershipPath,
|
||||
`${JSON.stringify({
|
||||
schema_version: 1,
|
||||
run_id: session.runId,
|
||||
pid: started.ownership.leaderPid,
|
||||
process_group_id: started.ownership.processGroupId,
|
||||
|
||||
@@ -65,26 +65,14 @@ describe("Ghidra provider", () => {
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
operation: "list_procedures",
|
||||
pagination: "none",
|
||||
effects: expect.objectContaining({
|
||||
mutatesArtifact: false,
|
||||
mayShowUi: false,
|
||||
mayWriteFilesystem: true,
|
||||
}),
|
||||
limits: {
|
||||
maxResults: null,
|
||||
maxPayloadBytes: 1024 * 1024,
|
||||
timeoutMs: 10_000,
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operation: "analyze_function",
|
||||
pagination: "none",
|
||||
limits: {
|
||||
maxResults: null,
|
||||
maxPayloadBytes: 1024 * 1024,
|
||||
timeoutMs: 35_000,
|
||||
},
|
||||
limitations: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
"unresolved targetless flows remain unknown",
|
||||
|
||||
@@ -39,7 +39,6 @@ describe("Ghidra provider capabilities", () => {
|
||||
Reflect.set(capabilities, 0, { ...first, available: false });
|
||||
Reflect.set(first, "available", false);
|
||||
Reflect.set(first.effects, "mutatesArtifact", true);
|
||||
Reflect.set(first.limits, "maxResults", 0);
|
||||
Reflect.set(first.limitations, 0, "forged limitation");
|
||||
|
||||
expect(provider.capabilities()).toEqual(published);
|
||||
|
||||
@@ -4,11 +4,6 @@ import type {
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import { WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON } from "../process/WindowsAuthority.js";
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
|
||||
import {
|
||||
GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS,
|
||||
GHIDRA_MAX_LINE_BYTES,
|
||||
GHIDRA_REQUEST_TIMEOUT_MS,
|
||||
} from "./GhidraDefaults.js";
|
||||
import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js";
|
||||
import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryValues.js";
|
||||
|
||||
@@ -39,12 +34,6 @@ export const GHIDRA_PROVIDER_TOOL_CONTRACTS = Object.freeze(
|
||||
),
|
||||
);
|
||||
|
||||
const PAGINATED_OPERATIONS: ReadonlySet<string> = new Set([]);
|
||||
const DECOMPILE_OPERATIONS: ReadonlySet<string> = new Set([
|
||||
"procedure_pseudo_code",
|
||||
"analyze_function",
|
||||
]);
|
||||
|
||||
/** Health limitations shared by every Ghidra-backed capability. */
|
||||
export const healthLimitations = Object.freeze([
|
||||
"The session serves operations only after default Ghidra auto-analysis completes; an analysis timeout fails the open instead of exposing partial results.",
|
||||
@@ -157,10 +146,6 @@ export const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
|
||||
operation,
|
||||
available: true,
|
||||
reason: null,
|
||||
pagination: PAGINATED_OPERATIONS.has(contract.name)
|
||||
? ("offset" as const)
|
||||
: ("none" as const),
|
||||
exhaustive: !PAGINATED_OPERATIONS.has(contract.name),
|
||||
effects: Object.freeze({
|
||||
mutatesArtifact: false,
|
||||
launchesProcess: true,
|
||||
@@ -170,13 +155,6 @@ export const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
}),
|
||||
limits: Object.freeze({
|
||||
maxResults: PAGINATED_OPERATIONS.has(contract.name) ? 500 : null,
|
||||
maxPayloadBytes: GHIDRA_MAX_LINE_BYTES,
|
||||
timeoutMs: DECOMPILE_OPERATIONS.has(contract.name)
|
||||
? GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS
|
||||
: GHIDRA_REQUEST_TIMEOUT_MS,
|
||||
}),
|
||||
limitations: Object.freeze(limitationsFor(contract.name)),
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -79,8 +79,7 @@ const parseTcpEndpoint = (
|
||||
typeof value !== "object" ||
|
||||
value === null ||
|
||||
Array.isArray(value) ||
|
||||
!hasExactKeys(value, ["host", "port", "schema_version"]) ||
|
||||
value.schema_version !== 1 ||
|
||||
!hasExactKeys(value, ["host", "port"]) ||
|
||||
value.host !== "127.0.0.1" ||
|
||||
!Number.isSafeInteger(value.port) ||
|
||||
typeof value.port !== "number" ||
|
||||
|
||||
@@ -193,7 +193,6 @@ export class HopperApplicationLauncher implements BridgeLauncher {
|
||||
lease?.release(),
|
||||
);
|
||||
const ownership = {
|
||||
schema_version: 1,
|
||||
run_id: session.runId,
|
||||
pid: started.ownership.leaderPid,
|
||||
process_group_id: started.ownership.leaderPid,
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("Hopper provider capabilities", () => {
|
||||
}
|
||||
expect(
|
||||
capabilities.find(({ operation }) => operation === "list_procedures"),
|
||||
).toMatchObject({ pagination: "none" });
|
||||
).toMatchObject({ available: true });
|
||||
expect(
|
||||
capabilities.find(({ operation }) => operation === "set_comment"),
|
||||
).toMatchObject({
|
||||
@@ -36,7 +36,6 @@ describe("Hopper provider capabilities", () => {
|
||||
Reflect.set(capabilities, 0, { ...first, available: false });
|
||||
Reflect.set(first, "available", false);
|
||||
Reflect.set(first.effects, "mutatesArtifact", true);
|
||||
Reflect.set(first.limits, "maxResults", 0);
|
||||
Reflect.set(first.limitations, 0, "forged limitation");
|
||||
|
||||
expect(provider.capabilities()).toEqual(published);
|
||||
|
||||
@@ -60,12 +60,6 @@ const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
|
||||
operation,
|
||||
available: true,
|
||||
reason: null,
|
||||
pagination:
|
||||
isRecord(contract.inputSchema.properties) &&
|
||||
"offset" in contract.inputSchema.properties
|
||||
? "offset"
|
||||
: "none",
|
||||
exhaustive: false,
|
||||
effects: Object.freeze({
|
||||
mutatesArtifact: MUTATING_OPERATIONS.has(contract.name),
|
||||
launchesProcess: true,
|
||||
@@ -75,11 +69,6 @@ const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
}),
|
||||
limits: Object.freeze({
|
||||
maxResults: null,
|
||||
maxPayloadBytes: contract.name === "read_bytes" ? 4_096 : null,
|
||||
timeoutMs: null,
|
||||
}),
|
||||
limitations: Object.freeze([
|
||||
"Results depend on Hopper's completed static analysis.",
|
||||
]),
|
||||
@@ -87,10 +76,6 @@ const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
|
||||
}),
|
||||
);
|
||||
|
||||
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/** Concrete analysis provider backed by REA's private Hopper bridge. */
|
||||
export class HopperProvider implements AnalysisProviderCandidate {
|
||||
constructor(
|
||||
|
||||
@@ -45,24 +45,18 @@ describe("Hopper provider discovery", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("advertises the exact bounded byte-read limit", () => {
|
||||
it("advertises byte reads as an available operation", () => {
|
||||
const capabilities = provider(process.execPath).capabilities();
|
||||
expect(
|
||||
capabilities.find(({ operation }) => operation === "read_bytes"),
|
||||
).toMatchObject({
|
||||
available: true,
|
||||
pagination: "none",
|
||||
limits: {
|
||||
maxResults: null,
|
||||
maxPayloadBytes: 4_096,
|
||||
timeoutMs: null,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
capabilities.find(
|
||||
({ operation }) => operation === "address_to_file_offset",
|
||||
),
|
||||
).toMatchObject({ available: true, pagination: "none" });
|
||||
).toMatchObject({ available: true });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
export const LINUX_PRIVATE_DISPLAY_DIAGNOSTIC_PREFIX = "REA_X11_DIAGNOSTIC_V1=";
|
||||
|
||||
const diagnosticContextSchema = z.object({
|
||||
schema_version: z.literal(1),
|
||||
component: z.literal("hopper_private_display"),
|
||||
operation: z.enum(["probe", "launch"]),
|
||||
reason: z
|
||||
|
||||
@@ -267,7 +267,6 @@ const syntheticDiagnostic = (
|
||||
reason: string,
|
||||
failureCode: HopperStartupFailureCode = "private_display_unavailable",
|
||||
): HopperStartupFailureDiagnostic => ({
|
||||
schema_version: 1,
|
||||
component: "hopper_private_display",
|
||||
operation: "probe",
|
||||
status: "error",
|
||||
|
||||
@@ -82,8 +82,6 @@ export class NativeMacOSProvider implements AnalysisProvider {
|
||||
provider: IDENTITY,
|
||||
operation: contract.name,
|
||||
...availability,
|
||||
pagination: "none" as const,
|
||||
exhaustive: contract.name !== "inspect_macho",
|
||||
effects: Object.freeze({
|
||||
mutatesArtifact: false,
|
||||
launchesProcess: true,
|
||||
@@ -93,11 +91,6 @@ export class NativeMacOSProvider implements AnalysisProvider {
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
}),
|
||||
limits: Object.freeze({
|
||||
maxResults: contract.name === "demangle_swift" ? 500 : null,
|
||||
maxPayloadBytes: OUTPUT_LIMIT,
|
||||
timeoutMs: TIMEOUT_MS,
|
||||
}),
|
||||
limitations: Object.freeze([
|
||||
"Availability and textual formats depend on the installed macOS/Xcode toolchain.",
|
||||
]),
|
||||
|
||||
@@ -126,7 +126,6 @@ const main = async (): Promise<void> => {
|
||||
}
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
schema_version: 1,
|
||||
left,
|
||||
...(request.right === undefined ? {} : { right }),
|
||||
}),
|
||||
|
||||
@@ -16,15 +16,11 @@ const outcome = {
|
||||
describe("replay worker protocol", () => {
|
||||
it("accepts only the exact committed case sequence", () => {
|
||||
expect(
|
||||
parseReplayWorkerResponse(
|
||||
{ schema_version: 1, left: [outcome] },
|
||||
cases,
|
||||
"single",
|
||||
),
|
||||
parseReplayWorkerResponse({ left: [outcome] }, cases, "single"),
|
||||
).toMatchObject({ left: [outcome] });
|
||||
expect(() =>
|
||||
parseReplayWorkerResponse(
|
||||
{ schema_version: 1, left: [{ ...outcome, case_id: "forged" }] },
|
||||
{ left: [{ ...outcome, case_id: "forged" }] },
|
||||
cases,
|
||||
"single",
|
||||
),
|
||||
@@ -32,7 +28,6 @@ describe("replay worker protocol", () => {
|
||||
expect(() =>
|
||||
parseReplayWorkerResponse(
|
||||
{
|
||||
schema_version: 1,
|
||||
left: [{ ...outcome, input_sha256: "2".repeat(64) }],
|
||||
},
|
||||
cases,
|
||||
@@ -43,16 +38,11 @@ describe("replay worker protocol", () => {
|
||||
|
||||
it("requires the exact differential shape and strict outcomes", () => {
|
||||
expect(() =>
|
||||
parseReplayWorkerResponse(
|
||||
{ schema_version: 1, left: [outcome] },
|
||||
cases,
|
||||
"differential",
|
||||
),
|
||||
parseReplayWorkerResponse({ left: [outcome] }, cases, "differential"),
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
parseReplayWorkerResponse(
|
||||
{
|
||||
schema_version: 1,
|
||||
left: [{ ...outcome, unexpected: true }],
|
||||
},
|
||||
cases,
|
||||
@@ -62,7 +52,6 @@ describe("replay worker protocol", () => {
|
||||
expect(() =>
|
||||
parseReplayWorkerResponse(
|
||||
{
|
||||
schema_version: 1,
|
||||
left: [{ ...outcome, outcome: "exception", value: undefined }],
|
||||
},
|
||||
cases,
|
||||
|
||||
@@ -30,14 +30,12 @@ const workerOutcomeSchema = z.discriminatedUnion("outcome", [
|
||||
|
||||
const singleWorkerResponseSchema = z
|
||||
.object({
|
||||
schema_version: z.literal(1),
|
||||
left: z.array(workerOutcomeSchema),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const differentialWorkerResponseSchema = z
|
||||
.object({
|
||||
schema_version: z.literal(1),
|
||||
left: z.array(workerOutcomeSchema),
|
||||
right: z.array(workerOutcomeSchema),
|
||||
})
|
||||
|
||||
@@ -151,7 +151,7 @@ const evidenceValues = (
|
||||
);
|
||||
if (!inventory.success) return [];
|
||||
if (kind === "manifest") return [inventory.data.manifest.manifest_id];
|
||||
return inventory.data.occurrences.items.map(({ occurrence_id: id }) => id);
|
||||
return inventory.data.occurrences.map(({ occurrence_id: id }) => id);
|
||||
};
|
||||
|
||||
const isProcessCaptureEvidence = (evidence: LedgerEvidence): boolean => {
|
||||
|
||||
@@ -58,8 +58,8 @@ const registerCommitTool = (
|
||||
options.permissionAuthority,
|
||||
{
|
||||
path: input.workspace_path,
|
||||
readCapability: "investigation_workspace_read",
|
||||
writeCapability: "investigation_workspace_write",
|
||||
readCapability: "reconstruction_coverage_read",
|
||||
writeCapability: "reconstruction_coverage_write",
|
||||
operation: commitContract.name,
|
||||
},
|
||||
);
|
||||
@@ -104,7 +104,7 @@ const registerQueryTool = (
|
||||
const authorized = await authorizeRootPermission(
|
||||
options.permissionAuthority,
|
||||
{
|
||||
capability: "investigation_workspace_read",
|
||||
capability: "reconstruction_coverage_read",
|
||||
roots: [input.workspace_path],
|
||||
access: "read",
|
||||
operation: queryContract.name,
|
||||
|
||||
@@ -29,31 +29,29 @@ export const registerArtifactComparisonTool = (
|
||||
};
|
||||
const left = resolveSessionEvidenceIds(
|
||||
session,
|
||||
input.left_evidence_ids,
|
||||
[input.left_evidence_id],
|
||||
expected,
|
||||
);
|
||||
if (!left.ok) return toCallToolResult(left, contract);
|
||||
const right = resolveSessionEvidenceIds(
|
||||
session,
|
||||
input.right_evidence_ids,
|
||||
[input.right_evidence_id],
|
||||
expected,
|
||||
);
|
||||
if (!right.ok) return toCallToolResult(right, contract);
|
||||
const computed = await runDerivedOperation(context, contract.name, () =>
|
||||
compareArtifacts(left.value, right.value, input.offset, input.limit),
|
||||
compareArtifacts(left.value, right.value),
|
||||
);
|
||||
if (!computed.ok) return toCallToolResult(computed, contract);
|
||||
const comparison = computed.value;
|
||||
const leftEvidenceIds = input.left_evidence_ids;
|
||||
const rightEvidenceIds = input.right_evidence_ids;
|
||||
const leftEvidenceIds = [input.left_evidence_id];
|
||||
const rightEvidenceIds = [input.right_evidence_id];
|
||||
const evidence = createEvidence(undefined, ARTIFACT_COMPARISON_PROVIDER, {
|
||||
predicateType: "rea.artifact-comparison/v1",
|
||||
operation: contract.name,
|
||||
parameters: {
|
||||
left_evidence_ids: leftEvidenceIds,
|
||||
right_evidence_ids: rightEvidenceIds,
|
||||
offset: input.offset,
|
||||
limit: input.limit,
|
||||
},
|
||||
result: jsonValueSchema.parse(comparison),
|
||||
confidence: "derived",
|
||||
@@ -75,8 +73,8 @@ export const registerArtifactComparisonTool = (
|
||||
|
||||
const artifactUnknownInput = (
|
||||
input: {
|
||||
readonly left_evidence_ids: readonly string[];
|
||||
readonly right_evidence_ids: readonly string[];
|
||||
readonly left_evidence_id: string;
|
||||
readonly right_evidence_id: string;
|
||||
readonly unknown_registry_approved?: true | undefined;
|
||||
},
|
||||
status: ReturnType<typeof compareArtifacts>["status"],
|
||||
@@ -89,8 +87,8 @@ const artifactUnknownInput = (
|
||||
severity:
|
||||
status === "unknown" || status === "truncated" ? "high" : "medium",
|
||||
domain: "artifact-comparison",
|
||||
supporting_evidence_ids: [...input.left_evidence_ids],
|
||||
contradicting_evidence_ids: [...input.right_evidence_ids],
|
||||
supporting_evidence_ids: [input.left_evidence_id],
|
||||
contradicting_evidence_ids: [input.right_evidence_id],
|
||||
required_authority: "shipped-artifact",
|
||||
required_confidence: "observed",
|
||||
required_environment: null,
|
||||
|
||||
@@ -31,13 +31,7 @@ export const registerBundleComparisonTool = (
|
||||
if (!left.ok) return toCallToolResult(left, contract);
|
||||
if (!right.ok) return toCallToolResult(right, contract);
|
||||
const computed = await runDerivedOperation(context, contract.name, () =>
|
||||
compareBundles(
|
||||
left.value,
|
||||
right.value,
|
||||
input.record_pairs,
|
||||
input.offset,
|
||||
input.limit,
|
||||
),
|
||||
compareBundles(left.value, right.value, input.record_pairs),
|
||||
);
|
||||
if (!computed.ok) return toCallToolResult(computed, contract);
|
||||
const comparison = computed.value;
|
||||
@@ -48,8 +42,6 @@ export const registerBundleComparisonTool = (
|
||||
left_bundle_sha256: comparison.left_bundle_sha256,
|
||||
right_bundle_sha256: comparison.right_bundle_sha256,
|
||||
record_pairs: input.record_pairs,
|
||||
offset: input.offset,
|
||||
limit: input.limit,
|
||||
},
|
||||
result: jsonValueSchema.parse(comparison),
|
||||
confidence: "derived",
|
||||
|
||||
@@ -17,6 +17,7 @@ import { mcpProgressReporter } from "./mcpProgress.js";
|
||||
import { logToolExecution } from "./toolLogging.js";
|
||||
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
||||
import { toCallToolResult } from "./toolResult.js";
|
||||
import { createArtifactExtractionDestination } from "../application/ArtifactExtractionDestination.js";
|
||||
|
||||
interface EvidenceToolRegistration {
|
||||
readonly logger: Logger;
|
||||
@@ -49,8 +50,15 @@ export const registerEvidenceTools = (
|
||||
message: "started",
|
||||
});
|
||||
const parameters = jsonObjectSchema.parse(input);
|
||||
const executionParameters =
|
||||
contract.name === "extract_artifact"
|
||||
? {
|
||||
...parameters,
|
||||
output_root: createArtifactExtractionDestination(),
|
||||
}
|
||||
: parameters;
|
||||
if (options.permissionAuthority !== undefined) {
|
||||
const request = permissionRequest(contract.name, parameters);
|
||||
const request = permissionRequest(contract.name, executionParameters);
|
||||
if (request !== undefined) {
|
||||
const authorized = await options.permissionAuthority.authorize(
|
||||
request,
|
||||
@@ -67,7 +75,7 @@ export const registerEvidenceTools = (
|
||||
options.logger,
|
||||
contract.name,
|
||||
() =>
|
||||
analysis.execute(contract.name, parameters, {
|
||||
analysis.execute(contract.name, executionParameters, {
|
||||
signal: context.mcpReq.signal,
|
||||
progress,
|
||||
}),
|
||||
|
||||
@@ -97,25 +97,3 @@ export const createServerIdentity = (input: {
|
||||
},
|
||||
} as const;
|
||||
};
|
||||
|
||||
/** Compact identity projection for CLI summaries; MCP retains full identity. */
|
||||
export const createServerIdentitySummary = (
|
||||
input: Parameters<typeof createServerIdentity>[0],
|
||||
) => {
|
||||
const identity = createServerIdentity(input);
|
||||
return {
|
||||
package: {
|
||||
name: identity.package.name,
|
||||
version: identity.package.version,
|
||||
},
|
||||
server: {
|
||||
name: identity.server.name,
|
||||
version: identity.server.version,
|
||||
},
|
||||
catalog: {
|
||||
counts: identity.catalog.counts,
|
||||
digests: identity.catalog.digests,
|
||||
},
|
||||
alignment: identity.alignment,
|
||||
} as const;
|
||||
};
|
||||
|
||||
@@ -78,7 +78,6 @@ async function runInlineEvidenceScenarios(
|
||||
expect(compared.isError).not.toBe(true);
|
||||
expect(compared.structuredContent).toMatchObject({
|
||||
result: {
|
||||
schema_version: 1,
|
||||
summary: { unknown: expect.any(Number) },
|
||||
coverage: { status: expect.any(String) },
|
||||
},
|
||||
@@ -94,7 +93,6 @@ async function runInlineEvidenceScenarios(
|
||||
expect(sourceCompared.isError).not.toBe(true);
|
||||
expect(sourceCompared.structuredContent).toMatchObject({
|
||||
result: {
|
||||
schema_version: 1,
|
||||
reference: {
|
||||
root_sha256: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference.root_sha256,
|
||||
},
|
||||
|
||||
@@ -116,7 +116,6 @@ describe("application workflow MCP parity", () => {
|
||||
expect(semantic.isError).not.toBe(true);
|
||||
expect(semantic.structuredContent).toMatchObject({
|
||||
result: {
|
||||
schema_version: 1,
|
||||
source_evidence_id: left.value.evidence_id,
|
||||
source_graph_id: analyzed.semantic_graph.graph_id,
|
||||
summary: { retained_seed_matches: 1 },
|
||||
|
||||
@@ -93,7 +93,6 @@ function createReplayScenario(root: string) {
|
||||
},
|
||||
javascriptReplayRunner: {
|
||||
execute: async (prepared) => ({
|
||||
schema_version: 1,
|
||||
plan_digest: prepared.publicPlan.plan_digest,
|
||||
outcomes: [
|
||||
{
|
||||
|
||||
@@ -60,7 +60,6 @@ describe("application workflow CLI parity", () => {
|
||||
expect(compared).toMatchObject({
|
||||
operation: "compare_application_versions",
|
||||
normalized_result: {
|
||||
schema_version: 1,
|
||||
summary: { unknown: expect.any(Number) },
|
||||
},
|
||||
});
|
||||
@@ -74,7 +73,7 @@ describe("application workflow CLI parity", () => {
|
||||
]);
|
||||
expect(sourceCompared).toMatchObject({
|
||||
operation: "compare_source_to_bundle",
|
||||
normalized_result: { schema_version: 1 },
|
||||
normalized_result: { reference: expect.any(Object) },
|
||||
});
|
||||
}, 20_000);
|
||||
|
||||
@@ -177,7 +176,7 @@ describe("application workflow CLI Evidence resolution", () => {
|
||||
);
|
||||
expect(compared).toMatchObject({
|
||||
operation: "compare_application_versions",
|
||||
normalized_result: { schema_version: 1 },
|
||||
normalized_result: { summary: expect.any(Object) },
|
||||
});
|
||||
|
||||
const missingRecord = await runCli(
|
||||
@@ -322,7 +321,6 @@ describe("application workflow CLI validation and replay", () => {
|
||||
"--json",
|
||||
]);
|
||||
expect(inline).toMatchObject({
|
||||
schema_version: 1,
|
||||
final_state: "complete",
|
||||
decisions: [{ outcome: "matched" }],
|
||||
transition_journal: [
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "../../../src/cliOutput.js";
|
||||
|
||||
const CLI_INTEGRATION_TIMEOUT_MS = 60_000;
|
||||
const CLI_VARIANT_TIMEOUT_MS = 120_000;
|
||||
|
||||
describe("CLI output argument and sanitization boundary", () => {
|
||||
it("rejects token windows that would corrupt structured output", () => {
|
||||
@@ -175,59 +174,4 @@ describe("compiled CLI output boundary", () => {
|
||||
},
|
||||
CLI_INTEGRATION_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
workspaceCliTest(
|
||||
"keeps operation failure status independent of output controls",
|
||||
async ({ cli }) => {
|
||||
const variants = [
|
||||
["--format", "toon"],
|
||||
["--format", "json"],
|
||||
["--format", "yaml"],
|
||||
["--format", "md"],
|
||||
["--format", "jsonl"],
|
||||
["--filter-output", "category", "--json"],
|
||||
["--token-limit", "5", "--json"],
|
||||
["--token-count", "--json"],
|
||||
];
|
||||
|
||||
for (const flags of variants) {
|
||||
const result = await cli.run({
|
||||
arguments: [
|
||||
...flags,
|
||||
"investigate-versions",
|
||||
"/tmp/left",
|
||||
"/tmp/right",
|
||||
"/tmp/workspace.json",
|
||||
],
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
exitCode: 1,
|
||||
stdout: expect.any(String),
|
||||
});
|
||||
}
|
||||
},
|
||||
CLI_VARIANT_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
workspaceCliTest(
|
||||
"keeps failure logs out of structured stdout",
|
||||
async ({ cli }) => {
|
||||
const result = await cli.run({
|
||||
arguments: [
|
||||
"--json",
|
||||
"investigate-versions",
|
||||
"/tmp/left",
|
||||
"/tmp/right",
|
||||
"/tmp/workspace.json",
|
||||
],
|
||||
environment: { REA_LOG_LEVEL: "error" },
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
exitCode: 1,
|
||||
stderr: expect.stringContaining('"status":"error"'),
|
||||
});
|
||||
expect(result.json).toMatchObject({ error: "ApprovalRequired" });
|
||||
},
|
||||
CLI_INTEGRATION_TIMEOUT_MS,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,7 +16,6 @@ describe("reconstruction obligation ledger CLI parity", () => {
|
||||
bindings: [],
|
||||
contradictions: [],
|
||||
},
|
||||
page: { offset: 0, limit: 50 },
|
||||
};
|
||||
const { stdout } = await execute(
|
||||
process.execPath,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user