Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7856d460e9 | ||
|
|
a17c7ef27d | ||
|
|
0190bcbef1 | ||
|
|
fbe442b1e2 | ||
|
|
305e72995b | ||
|
|
c5e8871bda | ||
|
|
809dfbfe48 | ||
|
|
adc4852f3a | ||
|
|
674364443d | ||
|
|
d43a461958 | ||
|
|
119954c701 |
@@ -7,15 +7,17 @@ on:
|
||||
env:
|
||||
HUSKY: "0"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release-please:
|
||||
name: Release Please
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
outputs:
|
||||
release_created: ${{ steps.release.outputs.release_created }}
|
||||
tag_name: ${{ steps.release.outputs.tag_name }}
|
||||
@@ -25,11 +27,61 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check out release pull request
|
||||
if: steps.release.outputs.prs_created == 'true'
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ fromJSON(steps.release.outputs.pr).headBranchName }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js for generated documentation
|
||||
if: steps.release.outputs.prs_created == 'true'
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.release.outputs.prs_created == 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Regenerate release documentation
|
||||
if: steps.release.outputs.prs_created == 'true'
|
||||
run: npm run docs:generate
|
||||
|
||||
- name: Commit canonical release catalog
|
||||
if: steps.release.outputs.prs_created == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_PR_BRANCH: ${{ fromJSON(steps.release.outputs.pr).headBranchName }}
|
||||
run: |
|
||||
changed_paths="$(git diff --name-only)"
|
||||
if [[ -z "${changed_paths}" ]]; then
|
||||
echo "Release documentation is already canonical."
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${changed_paths}" != "docs/product-catalog.json" ]]; then
|
||||
echo "docs:generate changed unexpected tracked paths:" >&2
|
||||
printf '%s\n' "${changed_paths}" >&2
|
||||
exit 1
|
||||
fi
|
||||
git diff --check
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add -- docs/product-catalog.json
|
||||
git commit -m "chore: normalize release product catalog"
|
||||
gh auth setup-git
|
||||
git push origin "HEAD:${RELEASE_PR_BRANCH}"
|
||||
|
||||
publish:
|
||||
name: Publish to npm
|
||||
needs: release-please
|
||||
if: needs.release-please.outputs.release_created == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "2.5.0"
|
||||
".": "2.5.1"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## [2.5.1](https://github.com/morluto/rea/compare/rea-agents-2.5.0...rea-agents-2.5.1) (2026-07-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ci:** normalize generated release metadata ([d43a461](https://github.com/morluto/rea/commit/d43a461958c7bef56a61f75d7f7da73b43e7c387))
|
||||
* **ci:** normalize release pull request metadata ([119954c](https://github.com/morluto/rea/commit/119954c701533491ccbc4de649592e059f3f5d39))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **browser:** lazy-load Playwright sessions ([305e729](https://github.com/morluto/rea/commit/305e72995bd52caee7ad8b42aae531b8924e7d0d))
|
||||
* **dotnet:** reuse authenticated artifact snapshots ([c5e8871](https://github.com/morluto/rea/commit/c5e8871bda737c52d6b09303f9c95e225bf845f2))
|
||||
* eliminate repeated work in analysis hot paths ([fbe442b](https://github.com/morluto/rea/commit/fbe442b1e2395b93d1bffa2c192ea5ac9a0fab34))
|
||||
* **evidence:** validate ledger mutations incrementally ([809dfbf](https://github.com/morluto/rea/commit/809dfbfe4868478bf62be40e5f9db35d1b52f6be))
|
||||
* **javascript:** reuse parsed source across analyses ([adc4852](https://github.com/morluto/rea/commit/adc4852f3aea5bd2847ce9efb53fe43ad030dde6))
|
||||
* **server:** cache advertised JSON schemas ([6743644](https://github.com/morluto/rea/commit/674364443dc12b89fe6444fdfb364f1b53086f90))
|
||||
|
||||
## [2.5.0](https://github.com/morluto/rea/compare/rea-agents-2.4.0...rea-agents-2.5.0) (2026-07-24)
|
||||
|
||||
|
||||
|
||||
@@ -530,7 +530,7 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@2.5.0", "mcp"]
|
||||
"args": ["-y", "rea-agents@2.5.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -183,7 +183,7 @@ rea uninstall --purge-data
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@2.5.0", "mcp"]
|
||||
"args": ["-y", "rea-agents@2.5.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -173,7 +173,7 @@ Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@2.5.0", "mcp"]
|
||||
"args": ["-y", "rea-agents@2.5.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -173,7 +173,7 @@ Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@2.5.0", "mcp"]
|
||||
"args": ["-y", "rea-agents@2.5.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -173,7 +173,7 @@ Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Win
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@2.5.0", "mcp"]
|
||||
"args": ["-y", "rea-agents@2.5.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"catalog_schema_version": 1,
|
||||
"package": {
|
||||
"name": "rea-agents",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.1",
|
||||
"sdk": {
|
||||
"server": "2.0.0-beta.4",
|
||||
"client": "2.0.0-beta.4",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rea-agents",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "8.0.4",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.1",
|
||||
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -3,6 +3,7 @@ import canonicalize from "canonicalize";
|
||||
import {
|
||||
createEvidenceBundle,
|
||||
parseEvidenceBundle,
|
||||
validateResidualUnknownAddition,
|
||||
type EvidenceBundle,
|
||||
} from "../domain/evidenceBundle.js";
|
||||
import {
|
||||
@@ -216,8 +217,6 @@ export class EvidenceLedger {
|
||||
} catch (cause: unknown) {
|
||||
return err(new UnknownRegistryError("invalid-transition", { cause }));
|
||||
}
|
||||
const pendingRecords = new Map(this.#records);
|
||||
pendingRecords.set(evidence.evidence_id, evidence);
|
||||
const existingUnknown = this.#unknownHeads.get(unknown.unknown_id);
|
||||
if (existingUnknown !== undefined) {
|
||||
if (
|
||||
@@ -225,20 +224,12 @@ export class EvidenceLedger {
|
||||
existingUnknown.revision_digest !== unknown.revision_digest
|
||||
)
|
||||
return err(new UnknownRegistryError("already-exists"));
|
||||
const checked = this.#validateCandidate(
|
||||
pendingRecords,
|
||||
this.#unknownRevisions,
|
||||
);
|
||||
const checked = this.#appendIncremental([evidence]);
|
||||
if (!checked.ok) return checked;
|
||||
this.#commit(pendingRecords, this.#unknownRevisions, checked.value);
|
||||
return ok(null);
|
||||
}
|
||||
pendingRecords.set(mutation.evidence_id, mutation);
|
||||
const pendingUnknowns = new Map(this.#unknownRevisions);
|
||||
pendingUnknowns.set(unknownRevisionKey(unknown), unknown);
|
||||
const checked = this.#validateCandidate(pendingRecords, pendingUnknowns);
|
||||
const checked = this.#appendIncremental([evidence, mutation], unknown);
|
||||
if (!checked.ok) return checked;
|
||||
this.#commit(pendingRecords, pendingUnknowns, checked.value);
|
||||
return ok(structuredClone(unknown));
|
||||
}
|
||||
|
||||
@@ -332,16 +323,68 @@ export class EvidenceLedger {
|
||||
}),
|
||||
);
|
||||
}
|
||||
const pendingRecords = new Map(this.#records);
|
||||
pendingRecords.set(mutationEvidence.evidence_id, mutationEvidence);
|
||||
const pendingUnknowns = new Map(this.#unknownRevisions);
|
||||
pendingUnknowns.set(unknownRevisionKey(unknown), unknown);
|
||||
const checked = this.#validateCandidate(pendingRecords, pendingUnknowns);
|
||||
const checked = this.#appendIncremental([mutationEvidence], unknown);
|
||||
if (!checked.ok) return checked;
|
||||
this.#commit(pendingRecords, pendingUnknowns, checked.value);
|
||||
return ok(structuredClone(unknown));
|
||||
}
|
||||
|
||||
#appendIncremental(
|
||||
records: readonly Evidence[],
|
||||
unknown?: ResidualUnknown,
|
||||
): Result<
|
||||
void,
|
||||
EvidenceIntegrityError | EvidenceLimitError | UnknownRegistryError
|
||||
> {
|
||||
const additions = new Map<string, Evidence>();
|
||||
let addedBytes = 0;
|
||||
for (const evidence of records) {
|
||||
const existing =
|
||||
additions.get(evidence.evidence_id) ??
|
||||
this.#records.get(evidence.evidence_id);
|
||||
if (existing !== undefined) {
|
||||
if (!recordsAgree(existing, evidence))
|
||||
return err(new EvidenceIntegrityError("Conflicting evidence record"));
|
||||
continue;
|
||||
}
|
||||
additions.set(evidence.evidence_id, evidence);
|
||||
addedBytes += serializedBytes(evidence);
|
||||
}
|
||||
const unknownKey =
|
||||
unknown === undefined ? undefined : unknownRevisionKey(unknown);
|
||||
if (unknownKey !== undefined && this.#unknownRevisions.has(unknownKey))
|
||||
return err(new UnknownRegistryError("integrity"));
|
||||
const unknownBytes = unknown === undefined ? 0 : serializedBytes(unknown);
|
||||
if (
|
||||
this.#exceedsRecordLimit(
|
||||
this.#records.size + additions.size,
|
||||
this.#unknownRevisions.size + (unknown === undefined ? 0 : 1),
|
||||
)
|
||||
)
|
||||
return err(new EvidenceLimitError("records", this.limits.maxRecords));
|
||||
if (this.#bytes + addedBytes + unknownBytes > this.limits.maxBytes)
|
||||
return err(new EvidenceLimitError("bytes", this.limits.maxBytes));
|
||||
if (unknown !== undefined)
|
||||
try {
|
||||
validateResidualUnknownAddition(
|
||||
unknown,
|
||||
{
|
||||
get: (id) => additions.get(id) ?? this.#records.get(id),
|
||||
has: (id) => additions.has(id) || this.#records.has(id),
|
||||
},
|
||||
this.#unknownHeads,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
return err(new UnknownRegistryError("integrity", { cause }));
|
||||
}
|
||||
for (const [id, evidence] of additions) this.#records.set(id, evidence);
|
||||
if (unknown !== undefined && unknownKey !== undefined) {
|
||||
this.#unknownRevisions.set(unknownKey, unknown);
|
||||
this.#unknownHeads.set(unknown.unknown_id, unknown);
|
||||
}
|
||||
this.#bytes += addedBytes + unknownBytes;
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
#validateCandidate(
|
||||
records: ReadonlyMap<string, Evidence>,
|
||||
unknowns: ReadonlyMap<string, ResidualUnknown>,
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import { analyzeJavaScriptStaticSource } from "../domain/javascriptStaticAnalysis.js";
|
||||
import { analyzeJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js";
|
||||
import { analyzeParsedJavaScriptStaticSource } from "../domain/javascriptStaticAnalysis.js";
|
||||
import { analyzeParsedJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js";
|
||||
import {
|
||||
DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS,
|
||||
type JavaScriptSemanticLimits,
|
||||
} from "../domain/javascriptSemanticIr.js";
|
||||
import { parseJavaScriptSource } from "../domain/javascriptSourceParser.js";
|
||||
import type {
|
||||
JavaScriptSourceRange,
|
||||
JavaScriptSourcePoint,
|
||||
JavaScriptStaticAnalysis,
|
||||
} from "../domain/javascriptStaticAnalysisTypes.js";
|
||||
import { failedJavaScriptStaticAnalysis } from "../domain/javascriptStaticAnalysisHelpers.js";
|
||||
import type {
|
||||
JavaScriptArtifactFile,
|
||||
JavaScriptArtifactFileSet,
|
||||
@@ -142,13 +144,24 @@ const analyzeArtifactFile = (
|
||||
state.truncatedScopes += 1;
|
||||
return;
|
||||
}
|
||||
const analysis = analyzeJavaScriptStaticSource(file.text.value, {
|
||||
maxAstNodes: remainingNodes,
|
||||
maxFindings: remainingFindings,
|
||||
maxModules: remainingModules,
|
||||
deadline: context.deadline,
|
||||
now: context.now,
|
||||
});
|
||||
const parsed = parseJavaScriptSource(file.text.value);
|
||||
if (parsed === null) {
|
||||
const analysis = failedJavaScriptStaticAnalysis();
|
||||
state.files.push({ file, javascript: analysis, semantic: null });
|
||||
state.parseFailures += 1;
|
||||
return;
|
||||
}
|
||||
const analysis = analyzeParsedJavaScriptStaticSource(
|
||||
file.text.value,
|
||||
parsed,
|
||||
{
|
||||
maxAstNodes: remainingNodes,
|
||||
maxFindings: remainingFindings,
|
||||
maxModules: remainingModules,
|
||||
deadline: context.deadline,
|
||||
now: context.now,
|
||||
},
|
||||
);
|
||||
const staticFindings = findingCount(analysis);
|
||||
const semanticFindingBudget = Math.max(
|
||||
0,
|
||||
@@ -164,7 +177,7 @@ const analyzeArtifactFile = (
|
||||
};
|
||||
const semantics =
|
||||
analysis.parse_status === "complete" || analysis.parse_status === "partial"
|
||||
? analyzeJavaScriptSemantics(file.text.value, semanticLimits)
|
||||
? analyzeParsedJavaScriptSemantics(parsed, semanticLimits)
|
||||
: null;
|
||||
state.files.push({
|
||||
file,
|
||||
|
||||
@@ -29,9 +29,19 @@ import type {
|
||||
BrowserScenarioSessionPort,
|
||||
} from "./BrowserScenarioSessionPort.js";
|
||||
import { BrowserScenarioCaptureBudget } from "./PlaywrightScenarioArtifacts.js";
|
||||
import { PlaywrightScenarioSessionFactory } from "./PlaywrightScenarioSession.js";
|
||||
|
||||
const OPERATION = "capture_browser_scenario" as const;
|
||||
let defaultFactory: Promise<BrowserScenarioSessionFactory> | undefined;
|
||||
|
||||
const lazyPlaywrightFactory: BrowserScenarioSessionFactory = {
|
||||
async open(scenario, options) {
|
||||
defaultFactory ??= import("./PlaywrightScenarioSession.js").then(
|
||||
({ PlaywrightScenarioSessionFactory }) =>
|
||||
new PlaywrightScenarioSessionFactory(),
|
||||
);
|
||||
return (await defaultFactory).open(scenario, options);
|
||||
},
|
||||
};
|
||||
|
||||
export const PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY: ProviderIdentity =
|
||||
Object.freeze({
|
||||
@@ -353,7 +363,7 @@ export class PlaywrightBrowserScenarioProvider
|
||||
implements BrowserScenarioCapturePort
|
||||
{
|
||||
constructor(
|
||||
private readonly factory: BrowserScenarioSessionFactory = new PlaywrightScenarioSessionFactory(),
|
||||
private readonly factory: BrowserScenarioSessionFactory = lazyPlaywrightFactory,
|
||||
) {}
|
||||
|
||||
identity(): ProviderIdentity {
|
||||
|
||||
@@ -239,7 +239,7 @@ const validateUnknownGraph = (
|
||||
|
||||
const validateVerifiedResolution = (
|
||||
unknown: ResidualUnknown,
|
||||
evidenceById: ReadonlyMap<string, Evidence>,
|
||||
evidenceById: Pick<ReadonlyMap<string, Evidence>, "get">,
|
||||
): void => {
|
||||
if (unknown.contradicting_evidence_ids.length > 0)
|
||||
throw new TypeError(
|
||||
@@ -260,6 +260,78 @@ const validateVerifiedResolution = (
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate one new residual-unknown revision against an already valid ledger.
|
||||
* This keeps ordinary mutations proportional to the affected revision while
|
||||
* full bundle imports continue to validate the complete graph.
|
||||
*/
|
||||
export const validateResidualUnknownAddition = (
|
||||
unknown: ResidualUnknown,
|
||||
evidenceById: Pick<ReadonlyMap<string, Evidence>, "get" | "has">,
|
||||
currentHeads: ReadonlyMap<string, ResidualUnknown>,
|
||||
): void => {
|
||||
const current = currentHeads.get(unknown.unknown_id);
|
||||
if (
|
||||
current === undefined
|
||||
? unknown.revision !== 1 || unknown.previous_revision_digest !== null
|
||||
: unknown.revision !== current.revision + 1 ||
|
||||
unknown.previous_revision_digest !== current.revision_digest
|
||||
)
|
||||
throw new TypeError(
|
||||
`Residual unknown ${unknown.unknown_id} revision chain is broken`,
|
||||
);
|
||||
for (const evidenceId of referencedEvidenceIds(unknown))
|
||||
if (!evidenceById.has(evidenceId))
|
||||
throw new TypeError(
|
||||
`Residual unknown references missing evidence ${evidenceId}`,
|
||||
);
|
||||
for (const relationship of unknown.relationships)
|
||||
if (
|
||||
relationship.unknown_id !== unknown.unknown_id &&
|
||||
!currentHeads.has(relationship.unknown_id)
|
||||
)
|
||||
throw new TypeError(
|
||||
`Residual unknown references missing unknown ${relationship.unknown_id}`,
|
||||
);
|
||||
if (unknown.resolution?.disposition === "verified")
|
||||
validateVerifiedResolution(unknown, evidenceById);
|
||||
for (const relationship of unknown.relationships)
|
||||
if (
|
||||
relationship.type === "depends-on" &&
|
||||
reachesUnknown(
|
||||
relationship.unknown_id,
|
||||
unknown.unknown_id,
|
||||
currentHeads,
|
||||
unknown,
|
||||
)
|
||||
)
|
||||
throw new TypeError("Residual unknown dependency graph contains a cycle");
|
||||
};
|
||||
|
||||
const reachesUnknown = (
|
||||
start: string,
|
||||
goal: string,
|
||||
currentHeads: ReadonlyMap<string, ResidualUnknown>,
|
||||
candidate: ResidualUnknown,
|
||||
): boolean => {
|
||||
const pending = [start];
|
||||
const visited = new Set<string>();
|
||||
while (pending.length > 0) {
|
||||
const id = pending.pop();
|
||||
if (id === undefined) break;
|
||||
if (id === goal) return true;
|
||||
if (visited.has(id)) continue;
|
||||
visited.add(id);
|
||||
const unknown =
|
||||
id === candidate.unknown_id ? candidate : currentHeads.get(id);
|
||||
if (unknown === undefined) continue;
|
||||
for (const relationship of unknown.relationships)
|
||||
if (relationship.type === "depends-on")
|
||||
pending.push(relationship.unknown_id);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const evidenceQualifies = (
|
||||
unknown: ResidualUnknown,
|
||||
evidence: Evidence,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { parse } from "@babel/parser";
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import {
|
||||
@@ -41,6 +40,10 @@ import {
|
||||
propertyName,
|
||||
range,
|
||||
} from "./javascriptStaticAnalysisHelpers.js";
|
||||
import {
|
||||
parseJavaScriptSource,
|
||||
type ParsedJavaScriptSource,
|
||||
} from "./javascriptSourceParser.js";
|
||||
|
||||
interface BindPatternInput {
|
||||
readonly pattern: t.Node;
|
||||
@@ -68,18 +71,19 @@ interface AddBindingInput {
|
||||
export const analyzeJavaScriptSemantics = (
|
||||
source: string,
|
||||
inputLimits: Partial<JavaScriptSemanticLimits> = {},
|
||||
): JavaScriptSemanticIr => {
|
||||
const file = parseJavaScriptSource(source);
|
||||
return file === null
|
||||
? failedJavaScriptSemanticIr()
|
||||
: analyzeParsedJavaScriptSemantics(file, inputLimits);
|
||||
};
|
||||
|
||||
/** Recover semantics from an already parsed JavaScript artifact. */
|
||||
export const analyzeParsedJavaScriptSemantics = (
|
||||
file: ParsedJavaScriptSource,
|
||||
inputLimits: Partial<JavaScriptSemanticLimits> = {},
|
||||
): JavaScriptSemanticIr => {
|
||||
const limits = { ...DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, ...inputLimits };
|
||||
let file: ReturnType<typeof parse>;
|
||||
try {
|
||||
file = parse(source, {
|
||||
sourceType: "unambiguous",
|
||||
errorRecovery: true,
|
||||
plugins: ["jsx", "typescript"],
|
||||
});
|
||||
} catch {
|
||||
return failedJavaScriptSemanticIr();
|
||||
}
|
||||
const state = createState(file.program, limits);
|
||||
collectDefinitions(file.program, state);
|
||||
const references = collectSemanticReferences(file.program, state);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { parse } from "@babel/parser";
|
||||
|
||||
/** Babel AST produced by REA's inert JavaScript parser boundary. */
|
||||
export type ParsedJavaScriptSource = ReturnType<typeof parse>;
|
||||
|
||||
/** Parse JavaScript or TypeScript once without retaining unused comment nodes. */
|
||||
export const parseJavaScriptSource = (
|
||||
source: string,
|
||||
): ParsedJavaScriptSource | null => {
|
||||
try {
|
||||
return parse(source, {
|
||||
sourceType: "unambiguous",
|
||||
errorRecovery: true,
|
||||
attachComment: false,
|
||||
plugins: ["jsx", "typescript"],
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { parse } from "@babel/parser";
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import { inspectElectronStaticNode } from "./electronStaticAnalysis.js";
|
||||
@@ -29,22 +28,28 @@ import type {
|
||||
JavaScriptStaticAnalysis,
|
||||
JavaScriptStaticAnalysisLimits,
|
||||
} from "./javascriptStaticAnalysisTypes.js";
|
||||
import {
|
||||
parseJavaScriptSource,
|
||||
type ParsedJavaScriptSource,
|
||||
} from "./javascriptSourceParser.js";
|
||||
|
||||
/** Parse one bounded JavaScript artifact and recover static structure only. */
|
||||
export const analyzeJavaScriptStaticSource = (
|
||||
source: string,
|
||||
limits: JavaScriptStaticAnalysisLimits,
|
||||
): JavaScriptStaticAnalysis => {
|
||||
let file: ReturnType<typeof parse>;
|
||||
try {
|
||||
file = parse(source, {
|
||||
sourceType: "unambiguous",
|
||||
errorRecovery: true,
|
||||
plugins: ["jsx", "typescript"],
|
||||
});
|
||||
} catch {
|
||||
return failedJavaScriptStaticAnalysis();
|
||||
}
|
||||
const file = parseJavaScriptSource(source);
|
||||
return file === null
|
||||
? failedJavaScriptStaticAnalysis()
|
||||
: analyzeParsedJavaScriptStaticSource(source, file, limits);
|
||||
};
|
||||
|
||||
/** Recover static structure from an already parsed JavaScript artifact. */
|
||||
export const analyzeParsedJavaScriptStaticSource = (
|
||||
source: string,
|
||||
file: ParsedJavaScriptSource,
|
||||
limits: JavaScriptStaticAnalysisLimits,
|
||||
): JavaScriptStaticAnalysis => {
|
||||
const accumulator = createJavaScriptAnalysisAccumulator();
|
||||
traverseStaticSource(source, file, accumulator, limits);
|
||||
addSourceMapDirectives(source, accumulator, limits.maxFindings);
|
||||
@@ -53,7 +58,7 @@ export const analyzeJavaScriptStaticSource = (
|
||||
|
||||
const traverseStaticSource = (
|
||||
source: string,
|
||||
file: ReturnType<typeof parse>,
|
||||
file: ParsedJavaScriptSource,
|
||||
accumulator: AnalysisAccumulator,
|
||||
limits: JavaScriptStaticAnalysisLimits,
|
||||
): void => {
|
||||
@@ -76,7 +81,7 @@ const traverseStaticSource = (
|
||||
|
||||
const finalizeStaticAnalysis = (
|
||||
source: string,
|
||||
file: ReturnType<typeof parse>,
|
||||
file: ParsedJavaScriptSource,
|
||||
accumulator: AnalysisAccumulator,
|
||||
limits: JavaScriptStaticAnalysisLimits,
|
||||
): JavaScriptStaticAnalysis => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
|
||||
import {
|
||||
@@ -85,6 +86,8 @@ export class ManagedStaticProvider implements AnalysisProvider {
|
||||
}
|
||||
|
||||
class ManagedStaticClient implements AnalysisClient {
|
||||
#snapshotBytes: Buffer | undefined;
|
||||
|
||||
constructor(private readonly target: BinaryTarget) {}
|
||||
|
||||
async execute(
|
||||
@@ -123,11 +126,12 @@ class ManagedStaticClient implements AnalysisClient {
|
||||
`Artifact size ${String(metadata.size)} exceeds max_file_bytes ${String(maxFileBytes)}.`,
|
||||
),
|
||||
);
|
||||
const bytes = await readFile(
|
||||
this.target.path,
|
||||
options?.signal === undefined ? undefined : { signal: options.signal },
|
||||
);
|
||||
if (bytes.length > maxFileBytes)
|
||||
const snapshot = this.#snapshotBytes;
|
||||
const observed =
|
||||
snapshot === undefined
|
||||
? await readManagedSnapshot(this.target.path, options?.signal)
|
||||
: await hashManagedSource(this.target.path, options?.signal);
|
||||
if (observed.byteLength > maxFileBytes)
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
IDENTITY.id,
|
||||
@@ -135,13 +139,16 @@ class ManagedStaticClient implements AnalysisClient {
|
||||
`Artifact grew beyond max_file_bytes ${String(maxFileBytes)} while it was read.`,
|
||||
),
|
||||
);
|
||||
const observedDigest = createHash("sha256").update(bytes).digest("hex");
|
||||
if (observedDigest !== this.target.sha256)
|
||||
if (observed.sha256 !== this.target.sha256)
|
||||
return err(
|
||||
new EvidenceIntegrityError(
|
||||
`Managed artifact digest changed after open: expected ${this.target.sha256}, observed ${observedDigest} at ${this.target.path}`,
|
||||
`Managed artifact digest changed after open: expected ${this.target.sha256}, observed ${observed.sha256} at ${this.target.path}`,
|
||||
),
|
||||
);
|
||||
const bytes = snapshot ?? observed.bytes;
|
||||
if (bytes === undefined)
|
||||
throw new TypeError("Managed snapshot bytes are unavailable");
|
||||
this.#snapshotBytes = bytes;
|
||||
const result = inspectManagedOperation(
|
||||
operation,
|
||||
parameters,
|
||||
@@ -164,10 +171,51 @@ class ManagedStaticClient implements AnalysisClient {
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
this.#snapshotBytes = undefined;
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
interface ManagedSourceObservation {
|
||||
readonly byteLength: number;
|
||||
readonly sha256: string;
|
||||
readonly bytes?: Buffer;
|
||||
}
|
||||
|
||||
const readManagedSnapshot = async (
|
||||
path: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ManagedSourceObservation> => {
|
||||
const bytes = await readFile(
|
||||
path,
|
||||
signal === undefined ? undefined : { signal },
|
||||
);
|
||||
return {
|
||||
byteLength: bytes.length,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
bytes,
|
||||
};
|
||||
};
|
||||
|
||||
const hashManagedSource = async (
|
||||
path: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ManagedSourceObservation> => {
|
||||
const digest = createHash("sha256");
|
||||
let byteLength = 0;
|
||||
const stream = createReadStream(
|
||||
path,
|
||||
signal === undefined ? undefined : { signal },
|
||||
);
|
||||
for await (const chunk of stream) {
|
||||
if (!Buffer.isBuffer(chunk))
|
||||
throw new TypeError("Managed source stream returned non-buffer data");
|
||||
byteLength += chunk.length;
|
||||
digest.update(chunk);
|
||||
}
|
||||
return { byteLength, sha256: digest.digest("hex") };
|
||||
};
|
||||
|
||||
const isManagedOperation = (
|
||||
operation: AnalysisOperation,
|
||||
): operation is ManagedToolName =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Generated from package.json and package-lock.json; do not edit. */
|
||||
export const PACKAGE_METADATA = {
|
||||
name: "rea-agents",
|
||||
version: "2.5.0", // x-release-please-version
|
||||
version: "2.5.1", // x-release-please-version
|
||||
serverSdkVersion: "2.0.0-beta.4",
|
||||
clientSdkVersion: "2.0.0-beta.4",
|
||||
coreSdkVersion: "2.0.0-beta.4",
|
||||
|
||||
@@ -2,6 +2,15 @@ import { z } from "zod";
|
||||
|
||||
import type { ToolContract } from "../contracts/toolContracts.js";
|
||||
|
||||
const ADVERTISED_INPUT_SCHEMAS = new WeakMap<
|
||||
z.ZodObject,
|
||||
Readonly<Record<string, unknown>>
|
||||
>();
|
||||
const ADVERTISED_OUTPUT_SCHEMAS = new WeakMap<
|
||||
z.ZodObject,
|
||||
Readonly<Record<string, unknown>>
|
||||
>();
|
||||
|
||||
/** Project the public contract fields accepted by MCP tool registration. */
|
||||
export const toolRegistrationOptions = (contract: ToolContract) => ({
|
||||
title: contract.title,
|
||||
@@ -22,10 +31,8 @@ const advertisedInputSchema = (schema: z.ZodObject) => ({
|
||||
vendor: "rea",
|
||||
validate: (input: unknown) => ({ value: input }),
|
||||
jsonSchema: {
|
||||
input: () =>
|
||||
describeProperties(closeObjectSchemas(inputJsonSchema(schema))),
|
||||
output: () =>
|
||||
describeProperties(closeObjectSchemas(inputJsonSchema(schema))),
|
||||
input: () => advertisedInputJsonSchema(schema),
|
||||
output: () => advertisedInputJsonSchema(schema),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -41,19 +48,42 @@ const advertisedOutputSchema = (schema: z.ZodObject) => {
|
||||
"~standard": {
|
||||
...standard,
|
||||
jsonSchema: {
|
||||
input: () =>
|
||||
describeRootProperties(
|
||||
closeObjectSchemas(converter({ target: "draft-2020-12" })),
|
||||
),
|
||||
output: () =>
|
||||
describeRootProperties(
|
||||
closeObjectSchemas(converter({ target: "draft-2020-12" })),
|
||||
),
|
||||
input: () => advertisedOutputJsonSchema(schema, converter),
|
||||
output: () => advertisedOutputJsonSchema(schema, converter),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const advertisedInputJsonSchema = (
|
||||
schema: z.ZodObject,
|
||||
): Readonly<Record<string, unknown>> =>
|
||||
cachedJsonSchema(ADVERTISED_INPUT_SCHEMAS, schema, () =>
|
||||
describeProperties(closeObjectSchemas(inputJsonSchema(schema))),
|
||||
);
|
||||
|
||||
const advertisedOutputJsonSchema = (
|
||||
schema: z.ZodObject,
|
||||
converter: NonNullable<z.ZodObject["~standard"]["jsonSchema"]>["output"],
|
||||
): Readonly<Record<string, unknown>> =>
|
||||
cachedJsonSchema(ADVERTISED_OUTPUT_SCHEMAS, schema, () =>
|
||||
describeRootProperties(
|
||||
closeObjectSchemas(converter({ target: "draft-2020-12" })),
|
||||
),
|
||||
);
|
||||
|
||||
const cachedJsonSchema = (
|
||||
cache: WeakMap<z.ZodObject, Readonly<Record<string, unknown>>>,
|
||||
schema: z.ZodObject,
|
||||
create: () => Record<string, unknown>,
|
||||
): Readonly<Record<string, unknown>> => {
|
||||
const cached = cache.get(schema);
|
||||
if (cached !== undefined) return cached;
|
||||
const created = create();
|
||||
cache.set(schema, created);
|
||||
return created;
|
||||
};
|
||||
|
||||
const inputJsonSchema = (schema: z.ZodObject): Record<string, unknown> => {
|
||||
const converter = schema["~standard"].jsonSchema?.input;
|
||||
if (converter === undefined)
|
||||
|
||||
@@ -62,6 +62,21 @@ const emptySchemaPaths = (value: unknown, path = "$"): string[] => {
|
||||
};
|
||||
|
||||
describe("tool contract surface", () => {
|
||||
it("reuses finalized advertised schemas", () => {
|
||||
const contract = TOOL_CONTRACTS[0];
|
||||
expect(contract).toBeDefined();
|
||||
if (contract === undefined) return;
|
||||
|
||||
const first = toolRegistrationOptions(contract);
|
||||
const second = toolRegistrationOptions(contract);
|
||||
expect(first.inputSchema["~standard"].jsonSchema.input()).toBe(
|
||||
second.inputSchema["~standard"].jsonSchema.output(),
|
||||
);
|
||||
expect(first.outputSchema["~standard"].jsonSchema.input()).toBe(
|
||||
second.outputSchema["~standard"].jsonSchema.output(),
|
||||
);
|
||||
});
|
||||
|
||||
it("gives fallback parameter guidance operational meaning", () => {
|
||||
const contract = TOOL_CONTRACTS.find(
|
||||
({ name }) => name === "analyze_function",
|
||||
|
||||
Reference in New Issue
Block a user