跳到正文
DEV Community· jackymenCZ (jackymenCZ)·· 4 小時前AI 評分39

Sentinel-IR 0.1.3 上架 npm,改善 AST 別名偵測

Sentinel-IR 0.1.3 Is Live: Better AST Alias Detection. #Termux #mobile #android

AI 導讀

Sentinel-IR 0.1.3 已在 npm 上架,改進 JavaScript AST 分析中的別名鏈追蹤及作用域感知偵測。它亦識別間接 eval、方括號形式的環境變數讀取及靜態模板字面值鍵,並對環境變數讀取發出機密存取警告。本地發佈驗證中,79 項測試全部通過;npm 套件已另行安裝,並完成簽署及 attestation 核查。

正文

Release: sentinel-ir@0.1.3 on npm
Source code: GitLab repository
Release tag: v0.1.3
Changes since 0.1.2: Compare the previous release commit with v0.1.3
Sentinel-IR 0.1.3 is available on npm.
This release focuses on a problem that looks small until you start building code-analysis tools: the same operation can be written through different names, aliases, scopes, and property-access forms.
A useful analyzer must recognize relevant capabilities without treating every similar-looking name as the same thing.
This article follows three stages:
The first 30 seconds: What changed from 0.1.2?
About 10 minutes: An explanation for people who are new to programming, ASTs, and AI agents.
About 30 minutes: A technical walkthrough with JavaScript examples, regression tests, Android Termux commands, public npm installation, and release verification.
The examples analyze source code as text. They do not execute the potentially dangerous operations shown in the fixtures.

The first 30 seconds: what changed in 0.1.3?
Sentinel-IR converts JavaScript source into a compact structural representation that another component, such as an AI coding agent, can use for reasoning.
Version 0.1.2 already improved detection of dynamic code, computed calls, local shadowing, and static child_process imports.
Version 0.1.3 builds on that work.
The main changes:
Alias chains: follows selected local aliases of process-spawning methods, such as cp.execSync → run → runAgain.
Scope-aware alias handling: avoids reporting a process-spawning call when a function parameter or a local variable shadows an alias associated with child_process.
Indirect eval: recognizes the form (0, eval)(input) while respecting a local binding that shadows eval.
Bracket notation for environment variables: detects reads such as process.env["API_KEY"] and emits a secret-access warning.
Static template-literal keys: recognizes globalThis\eval`without automatically treating an interpolated key such asglobalThiseval${suffix}as a knowneval` key.
Release validation: 79 tests passed with zero failures in the local release verification. The published package was installed independently from npm, its signature and attestation checks were run, and its src/ast-facts.js hash matched the same file in the release's Git tag.
This is a focused improvement in structural evidence, not a claim that Sentinel-IR can understand every JavaScript program.
Now let's unpack why these details matter.

Part 1: No programming degree required
Approximately 10 minutes

  1. Think of Sentinel-IR as a technical inspector Imagine asking someone to repair a building. You could give them every document you have: photographs, email conversations, receipts, old plans, maintenance records, and every message exchanged since the building was constructed. That is a lot of information. It is not necessarily the best information for the first inspection. Alternatively, you could give them a useful structural map: A water pipe runs behind this wall. Electricity enters through this section. This door leads outside. The foundations need closer inspection. The current plans do not cover one part of the building. The map does not replace the building. It makes the first investigation more focused. Sentinel-IR applies a related idea to JavaScript source code. Rather than immediately passing an entire source file to an AI model, an application can extract structural facts such as: Which modules the file imports. Which routes a server declares. Which environment variables it reads. Which filesystem and network operations it contains. Whether it uses dynamic code execution or process-spawning APIs. Which syntax and source lines provide evidence for detected signals. The output is an intermediate representation, or IR. An AI agent can use this representation to decide where to investigate, which file deserves attention, or which constraints matter before proposing a change. The key idea is simple: Give the next component useful evidence instead of asking it to guess everything from raw source text.
  2. What is an AST? AST stands for Abstract Syntax Tree. A parser converts source code into a structured representation of its syntax. Consider these examples: eval(input);

parser.eval(input);

function evaluate(value) {
return value * 2;
}

All three contain text resembling eval.
Their meaning is not the same.
The first normally calls JavaScript's global eval function, unless that name has been replaced by a local binding.
The second calls a property named eval on an object called parser.
The third declares an ordinary function named evaluate.
A detector that merely searches for the substring "eval" cannot reliably distinguish these situations.
An AST provides more useful information about the actual syntax: which node represents a call, what expression is being called, what object a property belongs to, and where the expression appears.
Sentinel-IR uses Tree-sitter's JavaScript grammar to extract these structural facts.
That is the foundation for the improvements in both 0.1.2 and 0.1.3.

  1. Why aliases make code analysis harder Developers rarely write every operation in exactly the same form. Consider: const cp = require("child_process");

cp.execSync("example");

The relationship is straightforward: cp refers to the imported child_process module, and execSync is one of its process-spawning methods.
Now consider:
const cp = require("child_process");

const run = cp.execSync;

run("example");

The call uses run, not the original property expression.
A detector that recognizes only the direct form may fail to connect the final call to the capability from which it originated.
The same relationship can be extended:
const cp = require("child_process");

const run = cp.execSync;
const runAgain = run;

runAgain("example");

There are now two alias assignments between the imported capability and the call.
Version 0.1.3 adds regression coverage for this chain.
This does not mean the analyzer has become a complete JavaScript interpreter. It means that selected local alias patterns are handled more accurately.
That difference is important.
A narrow, tested improvement is more useful than a broad claim that aliases are now universally understood.

  1. What does variable shadowing mean? JavaScript allows a local variable or function parameter to use the same name as something declared elsewhere. For example: const run = someSensitiveOperation;

function helper(run) {
run("ordinary");
}

Inside helper, the name run refers to the function parameter.
It does not automatically refer to the outer variable.
The local parameter shadows the outer name.
This matters for security analysis because the analyzer must not assume that every occurrence of run refers to the same capability.
Version 0.1.3 adds regression cases for situations such as:
const { execSync: run } = require("child_process");

function helper(run) {
run("ordinary");
}

The outer run is associated with child_process. The parameter inside helper is a different binding.
The inner call must not be classified as a process-spawning call merely because the two bindings share a name.
There is a subtle distinction here: the file may still import a powerful capability. That is different from proving that this particular call launches a process.
Sentinel-IR preserves the distinction between the capability and the detected operation.
Its overall risk policy can still classify a file as critical when a critical capability is present. That is a conservative policy decision, not proof that a command was executed.

  1. Why does (0, eval)(input) matter? Most developers recognize this form immediately: eval(input);

But JavaScript supports other ways of invoking the same global function.
One example is:
(0, eval)(input);

The comma operator evaluates its expressions and returns the value of the last one.
Here, the resulting call uses eval as a value rather than using the straightforward direct-call form.
The distinction between direct and indirect eval can affect JavaScript execution semantics.
For static analysis, the important part is that a detector focused only on a simple eval(...) call can miss alternative syntax.
Version 0.1.3 adds a regression test for this exact form.
It also adds the opposite test:
function helper(eval) {
(0, eval)(input);
}

Here, the function parameter shadows the global name.
A useful analyzer must distinguish the two examples rather than flagging every expression that happens to use the word eval.

  1. Why inspect process.env["API_KEY"]? Environment variables commonly provide configuration, credentials, and other values that should be handled carefully. A program might read an environment variable using dot notation: const key = process.env.API_KEY;

Or it might use bracket notation:
const key = process.env["API_KEY"];

Both are ordinary ways to access an object property.
If an analyzer recognizes only the first style, the second can fall through the cracks.
Version 0.1.3 adds coverage for bracket access to a secret-shaped environment-variable name.
The expected findings include:
The environment variable name API_KEY.
A secret_access risk signal.
Evidence identifying the access as process.env.API_KEY.
This does not mean the analyzer has read or printed the actual secret value. It analyzes source code and reports the syntactic access.
It also does not prove that the program leaks the value. Reading a key and transmitting it to an external destination are different behaviors.
The purpose is to identify a useful point for further inspection.

  1. Static keys versus keys generated at runtime JavaScript offers both dot notation and computed property access. For example: globalThis.eval(input);

globalThis"eval";

Template literals provide another form:
globalThiseval;

The last example may look unusual, but the property name is statically known. Its value is simply eval.
Now compare it with:
globalThiseval${suffix};

The property name depends on suffix.
The analyzer cannot safely assume that its runtime value will be eval.
Version 0.1.3 tests both situations:
Source form
Expected interpretation
globalThis\eval`
Recognize the known static key
globalThis[\eval${suffix}
](input)`
Do not assume the key is a fixed eval

That is the kind of distinction that matters when an analyzer needs to avoid both missed findings and unsupported assumptions.

  1. Does a critical risk result mean the code is malicious? No. A risk label is not a verdict on intent. Sentinel-IR uses a risk policy to summarize structural findings. A file classified as critical may use dynamic code or import a capability that deserves careful review. That does not prove that the file is malicious, that the capability was exercised, or that an exploit exists. The opposite is equally important. A low risk result is not proof that a file is safe. Analysis can have limits, and an incomplete result must not be interpreted as evidence that dangerous behavior is absent. This is especially important when the output is used by another AI system. A model should receive evidence that it can inspect, together with the limitations of that evidence.
  2. What Sentinel-IR is designed to do Sentinel-IR is a structural input layer for reasoning about JavaScript code. It is useful when an application needs a compact description of a file before asking an AI model to help investigate, refactor, or assess it. The project is not trying to replace a complete development environment, a sandbox, a runtime monitor, or a full security audit. It is also not a JavaScript execution environment. The distinction matters because the more precisely a tool describes what it can and cannot establish, the more responsibly another component can use its results. That is the background to version 0.1.3. Now let's look at the actual implementation and tests.

Part 2: Developer deep dive
Approximately 30 minutes

  1. Version 0.1.2 versus 0.1.3 Version 0.1.2 improved several important parts of the detector: More explicit handling of global eval and Function forms. Better distinction between global names and locally shadowed names. Recognition of selected computed method calls. Static ES module import detection for child_process. Regression coverage for scope, imports, and global-object access. Version 0.1.3 concentrates on additional alias and syntax boundaries. Area 0.1.2 baseline 0.1.3 improvement Process-spawning aliases Recognizes selected direct and computed calls Follows selected chains of local aliases Alias shadowing Has earlier scope-aware checks Adds tests for parameters and local declarations that shadow spawn aliases Indirect eval Covers several direct and global-object forms Covers (0, eval)(input) Shadowed indirect eval Earlier checks account for local bindings Explicitly tests that an eval parameter is not treated as the global function Secret environment access Recognizes common environment reads Explicitly covers bracket access using a static secret-shaped key Static template-literal key Earlier computed-access support Recognizes globalThis[\eval] Interpolated property key Runtime-computed names remain a limitation Tests that an interpolated key is not assumed to equal eval Regression suite 69 passing tests in the 0.1.2 release report 79 passing tests in the 0.1.3 release verification

The last row is worth interpreting correctly.
The newer test count is evidence that the regression suite has expanded. It is not a benchmark proving that the analyzer understands every possible JavaScript program.
The release source and relevant files are available here:
Release tag v0.1.3
Release commit 918e3b1
Merged release commit 90ee762
New regression test file
AST extraction implementation

  1. The data model: facts, operations, and risk signals A useful way to inspect Sentinel-IR is to separate three concepts. Facts describe what the source structure contains. Operations summarize recognized capabilities or behaviors, such as dynamicEval and processSpawn. Risk signals attach evidence to specific findings. For example, a successful structural analysis may contain data conceptually like this: { "ast": true, "operations": { "dynamicEval": false, "processSpawn": true }, "riskSignals": [ { "signal": "process_spawn", "evidence": "runAgain", "line": 4 } ] }

This illustrates the shape of the information, not a guaranteed verbatim result for every fixture.
The actual evidence and line number depend on the supplied source.
A few distinctions are especially useful:
facts.ast: true means the source was parsed successfully for structural extraction.
A positive operation flag means the detector recognized a corresponding pattern.
A risk signal records an identified reason for concern.
An overall risk level applies the project's risk policy to the extracted information.
analysisStatus: "incomplete" indicates that structural extraction was unavailable or the source did not parse cleanly.
Do not collapse all these fields into a single judgment such as “safe” or “unsafe.”
The underlying facts and the policy-derived risk label serve different purposes.

  1. Alias chains: connecting a call with its origin Consider the following source: const cp = require("child_process");

const run = cp.execSync;
const runAgain = run;

runAgain("example");

A detector that checks only whether the final call is named execSync will miss the final expression, because its callee is runAgain.
The source-level relationship is:
child_process
|
v
cp.execSync
|
v
run
|
v
runAgain
|
v
call expression

The 0.1.3 regression test checks that the analyzer recognizes this selected chain as process spawning.
The corresponding assertions are:
assert.equal(facts.ast, true);
assert.equal(facts.operations.processSpawn, true);

assert.ok(
facts.riskSignals.some(
item => item.signal === "process_spawn"
)
);

These assertions test three separate things:
Structural parsing succeeded.
The operation was recognized.
The expected risk signal was produced.
This is stronger than simply searching the output for the string child_process.
Why track the operation separately from the imported capability?
An import by itself does not prove execution.
For instance:
const cp = require("child_process");

declares an imported capability, but it does not itself launch a command.
By contrast:
const cp = require("child_process");
const run = cp.execSync;

run("example");

contains a call that the analyzer is designed to classify as process spawning.
Sentinel-IR can conservatively assign critical risk when the child_process capability is present. The processSpawn operation remains a distinct fact.
That is a deliberate policy choice.
A downstream system should preserve the distinction rather than claiming that every file containing a child_process import has actually launched a process.
What is not guaranteed?
The regression demonstrates a selected local alias chain.
It does not establish general-purpose data-flow analysis across:
Multiple modules.
Arbitrarily computed property names.
Runtime mutation of objects or references.
Every reassignment or control-flow path.
Dynamically generated code.
Those require additional analysis techniques.
The implementation should therefore be understood as a structural analyzer with tested alias handling, not a complete JavaScript interpreter.

  1. Shadowing: why the same identifier may mean something different Now consider a destructured import: const { execSync: run } = require("child_process");

function helper(run) {
run("ordinary");
}

The module-level binding called run refers to the imported execSync method.
Inside helper, the parameter called run shadows that binding.
The call inside the function cannot be classified as process spawning solely because of the outer alias.
The expected regression assertion is:
assert.equal(facts.ast, true);
assert.equal(facts.operations.processSpawn, false);

The detector is being asked not only to find suspicious patterns, but also to avoid a specific unsupported conclusion.
A similar situation uses a local lexical declaration:
const cp = require("child_process");
const run = cp.execSync;

function helper() {
const run = value => value;

run("ordinary");
}

Inside the function, the local arrow function named run is the active binding.
The expected result is:
assert.equal(facts.operations.processSpawn, false);

This illustrates why name matching is insufficient for structural analysis.
A second shadowing boundary: the receiver itself
Consider:
const cp = require("child_process");

function helper(cp) {
cp.execSync("ordinary");
}

The parameter cp shadows the outer module binding.
The analyzer must not assume that the inner cp.execSync uses the imported module.
Again, the regression specifically checks:
assert.equal(facts.operations.processSpawn, false);

The file can still have a critical overall risk classification because the module capability exists in the outer scope.
That classification is separate from whether the inner call has been established as a process-spawning operation.
Why this matters for downstream AI decisions
Suppose an AI agent receives only a coarse label such as critical.
Without the supporting facts, it might incorrectly conclude that a particular function definitely launches a system process.
A more useful representation allows the consumer to inspect both the overall risk and the detected operations.
The principle is:
Preserve evidence and distinctions instead of allowing one summary label to replace the underlying analysis.

  1. Indirect eval: expanding the syntax coverage The 0.1.3 regression fixture is intentionally small: (0, eval)(input);

The test extracts structural facts and checks:
assert.equal(facts.ast, true);
assert.equal(facts.operations.dynamicEval, true);

assert.ok(
facts.riskSignals.some(
item => item.signal === "dynamic_code"
)
);

assert.equal(AstFacts.riskOf(facts), "critical");

The important part is not the number of characters in the fixture. It is that each expected property is independently asserted.
Now compare it with:
function helper(eval) {
(0, eval)(input);
}

The test expects:
assert.equal(facts.ast, true);
assert.equal(facts.operations.dynamicEval, false);

Inside helper, eval resolves to the parameter rather than automatically referring to the global function.
This is a recurring theme in static analysis: the correct result sometimes requires finding an operation, and sometimes requires explicitly not finding it.
What this does not establish
Recognizing an eval call does not determine whether its input is controlled by an attacker.
It also does not determine whether the code string will succeed, what it will do at runtime, or whether a particular input is exploitable.
The analyzer identifies a syntactic capability. A broader security assessment needs additional context.

  1. Static template literals and computed property access The first fixture uses a known property name: globalThiseval;

The regression expects:
assert.equal(facts.ast, true);
assert.equal(facts.operations.dynamicEval, true);

The second fixture uses interpolation:
globalThiseval${suffix};

The regression expects:
assert.equal(facts.ast, true);
assert.equal(facts.operations.dynamicEval, false);

Why is this a useful distinction?
In the first example, the property name is fixed in the source.
In the second, the property name depends on an expression evaluated at runtime.
A detector that treats every template literal as a fixed property name would be guessing.
A detector that rejects every bracket expression would miss statically recognizable cases.
The useful boundary is between an identifiable static value and a value that has not been resolved.
Version 0.1.3 explicitly tests that boundary.

  1. Bracket notation for secret-shaped environment variables The new regression uses: const key = process.env["API_KEY"];

The assertions include:
assert.equal(facts.ast, true);
assert.ok(facts.env.includes("API_KEY"));

assert.ok(
facts.riskSignals.some(
item =>
item.signal === "secret_access" &&
item.evidence === "process.env.API_KEY"
)
);

Notice that the expected evidence is normalized to a meaningful property-access description.
The analyzer does not need to execute the code or read the actual environment to recognize the access pattern.
It also does not prove the presence of the named variable in the current machine's environment.
The analyzer is working from the source structure.
This distinction matters when building a scanning pipeline that should be deterministic, repeatable, and safe to run against untrusted source text.

  1. The 10 new release regression cases The new file is test/v013-regressions.test.js. It covers ten targeted cases: Test case Expected behavior Indirect global eval through the comma operator Recognize dynamic code Direct alias of a child_process method Recognize process spawning Parameter shadows a spawn alias Do not classify the shadowed call as spawning Bracket access to a secret-shaped environment key Report the environment key and secret-access signal Static template-literal property Recognize globalThis[\eval] Interpolated template-literal property Do not assume a fixed eval key Indirect eval with a shadowed parameter Do not classify it as global dynamic code Local lexical declaration shadows a spawn alias Do not classify the shadowed call as spawning Function parameter shadows a module binding Do not attribute the call to the outer module binding Process-spawn detection follows local alias chains Recognize the chain

The project test suite contains additional cases for earlier features.
The full release verification reported:
tests 79
pass 79
fail 0

The purpose of this table is not to suggest that ten fixtures exhaust all the possibilities in JavaScript. It is to show exactly which boundaries the new version has been tested against.
A small, reproducible regression test is useful because another developer can inspect the input, run the test, and challenge the expected result.

Part 3: Reproduce the public package check in Android Termux
Practical walkthrough
The following steps install the published npm package into a clean consumer directory.
They are separate from the source-repository tests later in this article.
That distinction is important: the published package and the development repository are not the same artifact.

  1. Check Node.js and the published release Open Termux. First, check the available versions: node --version npm --version

Sentinel-IR requires Node.js 20 or newer.
If you need Node.js in Termux, install it through Termux's package manager:
pkg update
pkg install nodejs git

If your environment already has an appropriate Node.js installation, you do not need to reinstall it.
Now query the public registry:
npm view sentinel-ir versions --json

npm view sentinel-ir dist-tags --json

npm view sentinel-ir@0.1.3 version

At the time of this release verification, the published version list included 0.1.3, and the latest distribution tag pointed to 0.1.3.
The version query should return:
0.1.3

A local package.json cannot prove that a public registry contains the release. Querying npm confirms the version available to consumers.

  1. Install the exact public package Create a separate directory: mkdir -p "$HOME/sentinel-ir-v013-consumer" cd "$HOME/sentinel-ir-v013-consumer"

npm init -y
npm install --save-exact --omit=optional sentinel-ir@0.1.3

npm ls sentinel-ir

The dependency tree should show:
sentinel-ir@0.1.3

Why use an exact version?
Because we want to test this release, not whichever version happens to be tagged latest in the future.
Why use --omit=optional?
The package includes its WebAssembly parser path. Omitting optional native parser dependencies gives this consumer test a straightforward way to exercise the bundled WebAssembly route on Android.
For that route, use the asynchronous API: analyzeAsync().
This is a release smoke test, not a recommendation to omit optional dependencies in every development environment.

  1. Create a reproducible public-package test Create the test script: cat > check-v013.js <<'NODE' const assert = require("node:assert/strict"); const { analyzeAsync } = require("sentinel-ir");

const cases = [
{
name: "indirect global eval is detected",
source: "(0, eval)(input);",
check(result) {
assert.equal(result.facts.operations.dynamicEval, true);

  assert.ok(
    result.facts.riskSignals.some(
      item => item.signal === "dynamic_code"
    )
  );

  assert.equal(result.risk, "critical");
}

},

{
name: "a shadowed eval parameter is not global eval",
source:
function helper(eval) {
(0, eval)(input);
}
,
check(result) {
assert.equal(result.facts.operations.dynamicEval, false);
}
},

{
name: "process-spawn detection follows an alias chain",
source: `
const cp = require("child_process");
const run = cp.execSync;
const runAgain = run;

  runAgain("example");
`,
check(result) {
  assert.equal(result.facts.operations.processSpawn, true);

  assert.ok(
    result.facts.riskSignals.some(
      item => item.signal === "process_spawn"
    )
  );

  assert.equal(result.risk, "critical");
}

},

{
name: "a parameter shadowing a spawn alias is not a spawn",
source: `
const { execSync: run } = require("child_process");

  function helper(run) {
    run("ordinary");
  }
`,
check(result) {
  assert.equal(result.facts.operations.processSpawn, false);
}

},

{
name: "a local function-scope declaration shadows the alias",
source: `
const cp = require("child_process");
const run = cp.execSync;

  function helper() {
    const run = value => value;
    run("ordinary");
  }
`,
check(result) {
  assert.equal(result.facts.operations.processSpawn, false);
}

},

{
name: "a parameter can shadow a module binding",
source: `
const cp = require("child_process");

  function helper(cp) {
    cp.execSync("ordinary");
  }
`,
check(result) {
  assert.equal(result.facts.operations.processSpawn, false);
}

},

{
name: "bracket access to API_KEY produces a secret warning",
source:
const key = process.env["API_KEY"];
,
check(result) {
assert.ok(result.facts.env.includes("API_KEY"));

  assert.ok(
    result.facts.riskSignals.some(
      item =>
        item.signal === "secret_access" &&
        item.evidence === "process.env.API_KEY"
    )
  );
}

},

{
name: "a static template-literal eval key is recognized",
source: 'globalThiseval;',
check(result) {
assert.equal(result.facts.operations.dynamicEval, true);
}
},

{
name: "an interpolated eval key is not assumed to be static",
source: 'globalThiseval${suffix};',
check(result) {
assert.equal(result.facts.operations.dynamicEval, false);
}
},

{
name: "an ordinary parser.eval method is not global eval",
source: `
const parser = {
eval(value) {
return value;
}
};

  parser.eval("ordinary text");
`,
check(result) {
  assert.equal(result.facts.operations.dynamicEval, false);
}

}
];

async function main() {
let passed = 0;

for (const item of cases) {
const result = await analyzeAsync(item.source, {
target: "v013-smoke-test.js"
});

assert.equal(
  result.analysisStatus,
  "complete",
  `${item.name}: AST analysis should complete`
);

item.check(result);

passed += 1;
console.log(`PASS: ${item.name}`);

}

console.log(\n${passed}/${cases.length} smoke tests passed.);
}

main().catch(error => {
console.error(error);
process.exitCode = 1;
});
NODE

Now run:
node check-v013.js

Expected result
The script should report ten passing smoke tests:
PASS: indirect global eval is detected
PASS: a shadowed eval parameter is not global eval
PASS: process-spawn detection follows an alias chain
PASS: a parameter shadowing a spawn alias is not a spawn
PASS: a local function-scope declaration shadows the alias
PASS: a parameter can shadow a module binding
PASS: bracket access to API_KEY produces a secret warning
PASS: a static template-literal eval key is recognized
PASS: an interpolated eval key is not assumed to be static
PASS: an ordinary parser.eval method is not global eval

10/10 smoke tests passed.

These are source-analysis tests.
For example, the analyzer receives the string runAgain("example") as input. It does not call runAgain or execute a system command.
That separation lets us test detection behavior without performing the operations represented in the test fixtures.

  1. Inspect the complete IR The compact public API also lets you inspect the output rather than checking only individual flags. Create another example: cat > inspect-v013.js <<'NODE' const { analyzeAsync, compress } = require("sentinel-ir");

const code = `
const cp = require("child_process");
const run = cp.execSync;
const runAgain = run;

runAgain("example");
`;

async function main() {
const result = await analyzeAsync(code, {
target: "example.js"
});

console.log(JSON.stringify(compress(result), null, 2));
}

main().catch(error => {
console.error(error);
process.exitCode = 1;
});
NODE

Run it:
node inspect-v013.js

Inspect these parts of the output:
analysisStatus
world.risk
facts.ast
facts.operations
facts.riskSignals
The precise evidence text and line numbers depend on the source supplied to the analyzer.
The purpose of the compressed output is to expose useful structural information without reproducing the entire source file.

  1. Verify the package's registry signatures The release should be checked from the perspective of someone who installs the public package, not only from the developer's local directory. Run: npm audit signatures

During this release verification, npm reported:
2 packages have verified registry signatures

1 package has a verified attestation

Those are results from the tested dependency tree and registry verification at that time. The exact counts may change when dependencies or npm's output change.
A signature check and a source-code comparison answer different questions:
A registry signature helps verify the package's registry signature metadata.
An attestation provides additional provenance information where available.
A source hash comparison checks whether a specific file in the published package matches the corresponding file in the tagged source.
No single one of these checks replaces the others.

Part 4: Run the full project test suite from the tagged source
The npm package deliberately contains runtime files and the bundled WebAssembly grammar, not the repository's complete development test suite.
To run all repository tests, clone the source project and check out the release tag.
Use a separate directory so that the experiment does not alter an existing working tree:
cd "$HOME"

git clone --branch v0.1.3 --depth 1 \
https://gitlab.com/jakubrojicek11/sentinel-ir.git \
sentinel-ir-v013-source-check

cd sentinel-ir-v013-source-check

npm ci
npm test

The repository's configured test command is:
node --test test/*.test.js

The 0.1.3 release verification reported:
tests 79
suites 0
pass 79
fail 0
cancelled 0
skipped 0
todo 0

If optional native parser dependencies behave differently in your Android environment, investigate the installation error separately. The bundled WebAssembly path and analyzeAsync() provide the portable analysis route, but a public-package smoke test and a complete development-suite run are different kinds of validation.
What about CI?
The GitLab CI configuration for v0.1.3 specifies a Node.js test matrix covering versions 20, 22, and 24.
Its tagged publish job uses Node.js 24, reruns the tests, checks that the Git tag matches the package version, and publishes with npm provenance.
That gives the release process several useful checkpoints:
The source test suite.
Cross-version CI testing.
A tag-to-package-version consistency check.
A public package installation test.
Registry signature and provenance checks.
Source-file hash verification.
These checkpoints are related, but they are not interchangeable.
A local test suite can pass even if a package archive was assembled incorrectly. A package can exist in the registry while a particular source fixture still has a detection bug. And a valid signature does not itself prove that the detection logic is correct.
The strongest release evidence comes from combining these checks.

Part 5: Verify that the public npm source matches the release tag
One of the most useful final checks is to compare the same source file in two places:
The public npm archive.
The v0.1.3 Git tag.
The release uses this source file:
src/ast-facts.js
The following commands assume you have both the public package archive and a local clone of the repository.
First, download the public tarball into a separate directory:
mkdir -p "$HOME/sentinel-ir-v013-registry-artifact"

npm pack sentinel-ir@0.1.3 \
--pack-destination "$HOME/sentinel-ir-v013-registry-artifact"

Then compare the AST source hashes:
cd "$HOME/sentinel-ir"

archive="$HOME/sentinel-ir-v013-registry-artifact/sentinel-ir-0.1.3.tgz"

npm_ast_hash="$(
tar -xOf "$archive" package/src/ast-facts.js |
sha256sum | cut -d' ' -f1
)"

git_ast_hash="$(
git show 'v0.1.3:src/ast-facts.js' |
sha256sum | cut -d' ' -f1
)"

echo "npm: $npm_ast_hash"
echo "Git: $git_ast_hash"

test "$npm_ast_hash" = "$git_ast_hash"
echo "OK: npm AST source matches the release tag"

For the verified 0.1.3 release, the matching file hash was:
61f6491f941c5e1364c7411d2afdd2ae945525e0958d7086462039c93a15e4fd

The public archive and a locally prepared archive can have different archive-level SHA-256 values because they are separately created tarballs.
That does not automatically indicate a source mismatch.
The meaningful comparison here is the hash of the same extracted source file.
In the release verification, the AST source hash matched between the published npm archive and the Git tag.
This is a narrow, reproducible integrity check. It does not establish that every file in the package is identical to every file in the repository, nor does it replace the registry's signature and provenance checks.
It does establish that the specific source file containing these new AST detection changes matched the tagged release.

Part 6: What this release does not solve
Good release notes should explain what improved without turning a narrow improvement into a universal security claim.
Sentinel-IR 0.1.3 still has important boundaries.
JavaScript scope
The package targets JavaScript through Tree-sitter's JavaScript grammar.
It does not claim full support for TypeScript, JSX-heavy code, or unrelated programming languages.
One file at a time
The analyzer does not combine all capabilities across an entire application.
A method imported through several modules, a value passed across module boundaries, and a call generated at runtime are not equivalent to a simple local alias chain.
Incomplete semantic resolution
The new alias tests cover selected patterns.
They do not establish complete resolution of all reassignment, control-flow, object-mutation, or runtime-computed-property cases.
Syntax is not runtime behavior
Detecting a suspicious call does not tell us every input that could reach it, whether the call is reachable, or what every execution would do.
Likewise, recognizing a capability does not prove that the capability has been exercised.
An incomplete analysis is not evidence of safety
Consumers should inspect analysisStatus.
When the result is incomplete, the analyzer may have used text-based fallback heuristics instead of successful AST extraction.
A low risk label in that state must not be treated as proof that the file is harmless.
Automated systems should abstain, request additional evidence, or use another analysis path rather than silently interpreting incomplete analysis as a clean result.
Compact IR still contains source-derived information
Sentinel-IR reduces the amount of raw source text that may need to be included in an AI prompt.
It does not eliminate prompt injection.
The IR can still contain module names, identifiers, route paths, environment variable names, and other source-derived values.
The downstream model must continue to treat the input as potentially untrusted.
The goal is to provide a smaller, more structured description, not to magically remove every risk associated with source code or AI systems.

Part 7: Why these small distinctions matter
A code-analysis tool can fail in two directions.
It can miss a potentially dangerous operation because the syntax differs from the form it recognizes.
It can also report a dangerous operation when the source structure does not support that conclusion.
Both problems matter when the output is used to guide another system.
Version 0.1.3 focuses on these distinctions:
A process-spawning capability can be referenced through a local alias chain.
A local binding can shadow an outer alias.
The same identifier can mean different things in different scopes.
An indirect global eval call can use a form other than eval(...).
A statically known template-literal property differs from an interpolated property.
Reading a secret-shaped environment variable is a different fact from transmitting its value.
A capability being present is not the same fact as proving that a particular operation was executed.
These are not glamorous changes.
They are exactly the sort of changes that benefit from small regression fixtures and explicit expected outcomes.
A test suite should not only say, “This source pattern is recognized.”
It should also say, “This similar-looking pattern is not classified the same way, and here is why.”
That makes the detector easier to inspect and its limits easier to understand.
For developers building AI coding agents, this distinction is especially valuable.
An agent should not have to infer every relevant capability from raw source text when a deterministic structural analysis can supply evidence first. But the evidence should still be treated as evidence, not as an infallible verdict.

Release summary
Sentinel-IR 0.1.3 is a focused update to JavaScript structural analysis.
It adds tested handling for selected local alias chains, shadowed spawn aliases, indirect global eval, bracket access to secret-shaped environment variables, and static template-literal property names.
The release verification included:
79 passing repository tests.
A public npm installation test.
Ten targeted consumer smoke tests.
Registry signature and attestation checks.
A matching src/ast-facts.js hash between the public npm archive and the Git release tag.
The published package is available here:
npm: https://www.npmjs.com/package/sentinel-ir/v/0.1.3
Source code: https://gitlab.com/jakubrojicek11/sentinel-ir
Release tag: https://gitlab.com/jakubrojicek11/sentinel-ir/-/tags/v0.1.3
Release pipeline: https://gitlab.com/jakubrojicek11/sentinel-ir/-/pipelines
To install the exact release:
npm install --save-exact sentinel-ir@0.1.3

To run the full source test suite:
npm ci
npm test

And to use the bundled WebAssembly parser through the public API:
const { analyzeAsync, compress } = require("sentinel-ir");

async function main() {
const result = await analyzeAsync(
'const cp = require("child_process"); ' +
'const run = cp.execSync; ' +
'const runAgain = run; ' +
'runAgain("example");',
{ target: "example.js" }
);

console.log(JSON.stringify(compress(result), null, 2));
}

main().catch(error => {
console.error(error);
process.exitCode = 1;
});

The example analyzes a source string. It does not execute the call represented in that string.

A question for developers
Which JavaScript syntax patterns cause your current tooling to confuse a name with the capability behind it?
Have you found cases where an alias, a shadowed binding, a computed property, or an indirect call caused an analyzer to miss something or report something incorrectly?
A minimal reproducible example is a good place to start.
That is how an assumption becomes a test, how a test becomes evidence, and how a focused release can improve the reliability of the next component in an AI-assisted development workflow.
Sentinel-IR 0.1.3 is live. The next step is to keep testing the boundaries.

來源:DEV Community · dev.to