@clear-capabilities/agentic-security-scanner 0.183.1 → 0.185.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/bin/agentic-security.js +88 -0
- package/dist/{2a8456e0800aeee871b6.json → 0b301d45b231f144a1b4.json} +27 -9
- package/dist/1128.index.js +908 -0
- package/dist/1336.index.js +261 -112
- package/dist/1665.index.js +828 -0
- package/dist/2923.index.js +17 -9
- package/dist/9382.index.js +23 -2
- package/dist/9579.index.js +11 -4
- package/dist/agentic-security.mjs +15 -15
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/frontend/src/shell.js +12 -2
- package/package.json +27 -9
- package/src/context-intelligence/controls.js +15 -0
- package/src/context-intelligence/reachability.js +8 -2
- package/src/dataflow/CLAUDE.md +43 -1
- package/src/dataflow/backward.js +42 -7
- package/src/dataflow/cross-service-taint.js +83 -1
- package/src/dataflow/demand-query.js +929 -0
- package/src/dataflow/dep-fingerprint.js +718 -0
- package/src/dataflow/engine.js +48 -6
- package/src/dataflow/ifds-precise.js +22 -0
- package/src/dataflow/incremental-replay.js +118 -0
- package/src/dataflow/incremental-session.js +257 -0
- package/src/dataflow/incremental.js +237 -35
- package/src/dataflow/index.js +25 -94
- package/src/dataflow/summaries.js +72 -6
- package/src/dataflow/tabulation.js +80 -24
- package/src/discovery/hunter.js +6 -2
- package/src/discovery/index.js +5 -2
- package/src/engine.js +63 -3
- package/src/graph/CLAUDE.md +31 -0
- package/src/graph/adapters/artifact.js +81 -0
- package/src/graph/adapters/crossrepo.js +75 -0
- package/src/graph/adapters/dataflow.js +76 -0
- package/src/graph/adapters/provenance.js +92 -0
- package/src/graph/adapters/semantic.js +89 -0
- package/src/graph/crosswalk-builders.js +152 -0
- package/src/graph/crosswalk.js +146 -0
- package/src/graph/index.js +15 -0
- package/src/graph/observation.js +57 -0
- package/src/graph/refs.js +105 -0
- package/src/graph/registry.js +129 -0
- package/src/graph/relations.js +81 -0
- package/src/graph/scc.js +241 -0
- package/src/graph/view.js +254 -0
- package/src/language/haskell-disclosure.js +13 -1
- package/src/lineage/CLAUDE.md +71 -8
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +1 -1
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +6 -0
- package/src/lineage/boundary-contract.js +273 -0
- package/src/lineage/completion.js +205 -0
- package/src/lineage/continuation.js +140 -0
- package/src/lineage/control-credit.js +313 -0
- package/src/lineage/correlation.js +199 -0
- package/src/lineage/coverage.js +8 -1
- package/src/lineage/cross-repo-link.js +22 -0
- package/src/lineage/cut-planner.js +548 -0
- package/src/lineage/driver.js +219 -66
- package/src/lineage/engine.js +166 -21
- package/src/lineage/export-json.js +5 -0
- package/src/lineage/graph-builder.js +84 -7
- package/src/lineage/haskell-view.js +59 -0
- package/src/lineage/index.js +3 -0
- package/src/lineage/nix-view.js +35 -0
- package/src/lineage/observation-adapters.js +17 -9
- package/src/lineage/owned-build.js +77 -0
- package/src/lineage/owned-worker-entry.js +22 -0
- package/src/lineage/owned-worker.js +95 -0
- package/src/lineage/path-evidence.js +123 -0
- package/src/lineage/path-predicates.js +529 -0
- package/src/lineage/path-query.js +189 -20
- package/src/lineage/path-store.js +191 -17
- package/src/lineage/scc-solver.js +186 -0
- package/src/lineage/smt-orchestrator.js +138 -0
- package/src/lineage/source-seeding.js +36 -2
- package/src/lineage/summaries.js +66 -4
- package/src/lineage/value-versions.js +130 -0
- package/src/lineage/witness-selection.js +95 -0
- package/src/pipeline/scan-health.js +19 -0
- package/src/posture/CLAUDE.md +13 -0
- package/src/posture/evidence-bundle.js +1 -1
- package/src/posture/witness-bundle.js +573 -0
- package/src/posture/witness-runtime.js +276 -0
- package/src/report/index.js +2 -0
package/src/lineage/engine.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { accessPathOf, pathIsCoveredByPrefix } from '../dataflow/access-paths.js';
|
|
2
|
+
import {
|
|
3
|
+
CORRELATION_VERSION_2, deriveChild, relPathOf, invocationIdOf, scopeForSubPath,
|
|
4
|
+
} from './correlation.js';
|
|
5
|
+
import { computeReachingDefinitions, ENTRY_VERSION } from './value-versions.js';
|
|
2
6
|
import { identitiesAt, emptyState, removeIdentitiesAt, addIdentity, joinStates, statesEqual, hashState } from './field-identity.js';
|
|
3
7
|
|
|
4
8
|
function noIdentity() {
|
|
@@ -57,6 +61,68 @@ function pathHasWildcard(path) {
|
|
|
57
61
|
return path.split('.').includes('*');
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
// --- Correlation version 2 (DAG-03) -------------------------------------
|
|
65
|
+
// Everything below is inert unless the caller set `ctx.correlationVersion`
|
|
66
|
+
// to 2 AND attached a recorder. With either missing, `sub` returns the ctx it
|
|
67
|
+
// was given (the same reference), no field is added to any hop, and the
|
|
68
|
+
// analysis result and the hop stream are byte-identical to version 1.
|
|
69
|
+
function isV2(ctx) {
|
|
70
|
+
return ctx?.correlationVersion === CORRELATION_VERSION_2 && typeof ctx.recordHop === 'function';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Enters one sub-expression: every hop emitted at or below it is stamped with
|
|
74
|
+
// the derivation path of the level that emitted it (the innermost stamp
|
|
75
|
+
// wins, because an inner wrapper's fields are already on the hop when the
|
|
76
|
+
// outer wrapper sees it).
|
|
77
|
+
function sub(ctx, seg, stamp) {
|
|
78
|
+
if (!isV2(ctx)) return ctx;
|
|
79
|
+
const derivation = deriveChild(ctx.derivation, seg);
|
|
80
|
+
const base = ctx.recordHop;
|
|
81
|
+
return { ...ctx, derivation, recordHop: (h) => base({ derivationId: derivation, ...stamp, ...h }) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The invocation id of the call expression currently being resolved.
|
|
85
|
+
function currentInvocation(ctx) {
|
|
86
|
+
const site = ctx?.siteInfo;
|
|
87
|
+
return invocationIdOf({ scope: site?.scope, nodeId: site?.nodeId, context: site?.context, derivation: ctx?.derivation });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Recorder-only context for resolving the `position`-th ACTUAL argument of a
|
|
91
|
+
// call during parameter binding (summaries.js). Version 1: exactly the
|
|
92
|
+
// recorder-only derivation it always was. Version 2: the argument subtree is
|
|
93
|
+
// stamped with the invocation, its actual position and its derivation, so an
|
|
94
|
+
// argument read can pair only with the binding of the same invocation and
|
|
95
|
+
// formal position.
|
|
96
|
+
export function argumentContext(ctx, position) {
|
|
97
|
+
if (!ctx?.recordHop) return undefined;
|
|
98
|
+
if (!isV2(ctx)) return { recordHop: ctx.recordHop };
|
|
99
|
+
const base = {
|
|
100
|
+
recordHop: ctx.recordHop, correlationVersion: ctx.correlationVersion,
|
|
101
|
+
derivation: ctx.derivation, siteInfo: ctx.siteInfo,
|
|
102
|
+
};
|
|
103
|
+
return sub(base, `arg${position}`, { callRole: 'arg', invocationId: currentInvocation(ctx), argPosition: position });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The invocation id of the call expression a call-level ctx is resolving (version 2). */
|
|
107
|
+
export function callInvocationId(ctx) {
|
|
108
|
+
return isV2(ctx) ? currentInvocation(ctx) : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The recorder a callee's own analysis must be handed (see `rawRecordHop`). */
|
|
112
|
+
export function calleeRecorder(ctx) {
|
|
113
|
+
return ctx?.rawRecordHop ?? ctx?.recordHop;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The out-half correlation stamp for a write of byPath entry `subPath` of the
|
|
117
|
+
// value built by `source` (rooted at derivation `root`).
|
|
118
|
+
function outStamp(ctx, source, subPath, root) {
|
|
119
|
+
if (!isV2(ctx)) return null;
|
|
120
|
+
const { scope, rest } = scopeForSubPath(source, subPath, root);
|
|
121
|
+
const allocKind = source?.kind === 'object' ? 'object'
|
|
122
|
+
: (source?.kind === 'ident' || source?.kind === 'member') ? 'alias' : null;
|
|
123
|
+
return { derivationScope: scope, relPath: rest, allocKind };
|
|
124
|
+
}
|
|
125
|
+
|
|
60
126
|
// --- Path provenance hop recording (Sub-project C, increment 1) ---------
|
|
61
127
|
// See DESIGN_PATH_PROVENANCE.md for the full design. Only two helpers below
|
|
62
128
|
// are new; everything else in this file is unchanged in shape, and the
|
|
@@ -132,6 +198,7 @@ export function resolveExprIdentities(state, expr, ctx) {
|
|
|
132
198
|
fromPath: key, toPath: null, dataElementId: id,
|
|
133
199
|
syntacticPath: key === path ? null : path,
|
|
134
200
|
widenReason: null, lossReason: null,
|
|
201
|
+
...(isV2(ctx) ? { relPath: relPathOf(key, path) } : null),
|
|
135
202
|
});
|
|
136
203
|
}
|
|
137
204
|
}
|
|
@@ -199,6 +266,7 @@ export function resolveExprIdentities(state, expr, ctx) {
|
|
|
199
266
|
fromPath: key, toPath: null, dataElementId: id,
|
|
200
267
|
syntacticPath: key === path ? null : path,
|
|
201
268
|
widenReason: null, lossReason: null,
|
|
269
|
+
...(isV2(ctx) ? { relPath: relPathOf(key, path) } : null),
|
|
202
270
|
});
|
|
203
271
|
}
|
|
204
272
|
}
|
|
@@ -218,7 +286,7 @@ export function resolveExprIdentities(state, expr, ctx) {
|
|
|
218
286
|
// `assign`/`object` already use) — a coarse/ancestor-level fact on the
|
|
219
287
|
// base conservatively applies to every field read off it, same
|
|
220
288
|
// reasoning as `identitiesAt`'s ancestor coverage for state-backed reads.
|
|
221
|
-
const base = resolveExprIdentities(state, expr.object, ctx);
|
|
289
|
+
const base = resolveExprIdentities(state, expr.object, sub(ctx, 'base'));
|
|
222
290
|
if (expr.prop === '*') {
|
|
223
291
|
// Same reasoning as the path-succeeds branch above: unknown key on a
|
|
224
292
|
// non-path base — conservatively use everything the base carries
|
|
@@ -282,7 +350,7 @@ export function resolveExprIdentities(state, expr, ctx) {
|
|
|
282
350
|
const flat = new Set();
|
|
283
351
|
const byPath = new Map();
|
|
284
352
|
for (const prop of expr.props) {
|
|
285
|
-
const r = resolveExprIdentities(state, prop.value, ctx);
|
|
353
|
+
const r = resolveExprIdentities(state, prop.value, sub(ctx, prop.spread ? 'sp' : `p:${prop.key}`));
|
|
286
354
|
for (const id of r.flat) flat.add(id);
|
|
287
355
|
// Path provenance (Sub-project C, increment 1, Task 2 instrumented
|
|
288
356
|
// site 2 of 4): one production/object in-half per id this property
|
|
@@ -358,8 +426,10 @@ export function resolveExprIdentities(state, expr, ctx) {
|
|
|
358
426
|
|
|
359
427
|
case 'array': {
|
|
360
428
|
const flat = new Set();
|
|
429
|
+
let ei = 0;
|
|
361
430
|
for (const el of expr.elements) {
|
|
362
|
-
|
|
431
|
+
ei += 1;
|
|
432
|
+
const r = resolveExprIdentities(state, el, sub(ctx, `e${ei}`));
|
|
363
433
|
for (const id of r.flat) flat.add(id);
|
|
364
434
|
}
|
|
365
435
|
// Path provenance (§10.1 `array` row): structure-flattening by
|
|
@@ -380,8 +450,10 @@ export function resolveExprIdentities(state, expr, ctx) {
|
|
|
380
450
|
|
|
381
451
|
case 'tpl': {
|
|
382
452
|
const flat = new Set();
|
|
453
|
+
let pi = 0;
|
|
383
454
|
for (const part of expr.parts) {
|
|
384
|
-
|
|
455
|
+
pi += 1;
|
|
456
|
+
const r = resolveExprIdentities(state, part, sub(ctx, `t${pi}`));
|
|
385
457
|
for (const id of r.flat) flat.add(id);
|
|
386
458
|
}
|
|
387
459
|
// Path provenance (§10.1 `tpl` row): transformation-bearing — the
|
|
@@ -404,8 +476,8 @@ export function resolveExprIdentities(state, expr, ctx) {
|
|
|
404
476
|
// structure-flattening by design, not a gap. See
|
|
405
477
|
// DESIGN_INTRAPROCEDURAL.md §4's "structure-preserving vs.
|
|
406
478
|
// structure-flattening" invariant.
|
|
407
|
-
const left = resolveExprIdentities(state, expr.left, ctx);
|
|
408
|
-
const right = resolveExprIdentities(state, expr.right, ctx);
|
|
479
|
+
const left = resolveExprIdentities(state, expr.left, sub(ctx, 'l'));
|
|
480
|
+
const right = resolveExprIdentities(state, expr.right, sub(ctx, 'r'));
|
|
409
481
|
const flat = new Set([...left.flat, ...right.flat]);
|
|
410
482
|
// Path provenance (§10.1 `binary` row): same as `tpl` — deliberately
|
|
411
483
|
// a separate case (not shared) per ADR §4, but the hop shape agrees.
|
|
@@ -428,8 +500,8 @@ export function resolveExprIdentities(state, expr, ctx) {
|
|
|
428
500
|
// byPath, merged per sub-path across both operands, the same way
|
|
429
501
|
// `union` merges per sub-path across branches. See
|
|
430
502
|
// DESIGN_INTRAPROCEDURAL.md §4.
|
|
431
|
-
const left = resolveExprIdentities(state, expr.left, ctx);
|
|
432
|
-
const right = resolveExprIdentities(state, expr.right, ctx);
|
|
503
|
+
const left = resolveExprIdentities(state, expr.left, sub(ctx, 'l'));
|
|
504
|
+
const right = resolveExprIdentities(state, expr.right, sub(ctx, 'r'));
|
|
433
505
|
const flat = new Set([...left.flat, ...right.flat]);
|
|
434
506
|
const byPath = new Map();
|
|
435
507
|
for (const r of [left, right]) {
|
|
@@ -461,8 +533,10 @@ export function resolveExprIdentities(state, expr, ctx) {
|
|
|
461
533
|
// collapse into one coarse flat blob. See DESIGN_INTRAPROCEDURAL.md §4.
|
|
462
534
|
const flat = new Set();
|
|
463
535
|
const byPath = new Map();
|
|
536
|
+
let bi = 0;
|
|
464
537
|
for (const branch of expr.branches) {
|
|
465
|
-
|
|
538
|
+
bi += 1;
|
|
539
|
+
const r = resolveExprIdentities(state, branch, sub(ctx, `b${bi}`));
|
|
466
540
|
for (const id of r.flat) flat.add(id);
|
|
467
541
|
for (const [subPath, ids] of r.byPath) {
|
|
468
542
|
const existing = byPath.get(subPath) ?? new Set();
|
|
@@ -529,6 +603,7 @@ export function resolveExprIdentities(state, expr, ctx) {
|
|
|
529
603
|
fromPath: null, toPath: null, dataElementId: id,
|
|
530
604
|
syntacticPath: null, widenReason: null, lossReason: null,
|
|
531
605
|
peerScope: summary.resolvedQid ?? null, peerContext: summary.resolvedContext ?? null,
|
|
606
|
+
...(isV2(ctx) ? { callRole: 'return', invocationId: currentInvocation(ctx), relPath: '' } : null),
|
|
532
607
|
});
|
|
533
608
|
}
|
|
534
609
|
}
|
|
@@ -577,11 +652,13 @@ export function resolveExprIdentities(state, expr, ctx) {
|
|
|
577
652
|
for (const id of identitiesAt(state, receiverPath)) flat.add(id);
|
|
578
653
|
}
|
|
579
654
|
} else if (expr.callee?.kind === 'member' && expr.callee.object) {
|
|
580
|
-
const receiver = resolveExprIdentities(state, expr.callee.object, ctx);
|
|
655
|
+
const receiver = resolveExprIdentities(state, expr.callee.object, sub(ctx, 'recv', { receiver: true }));
|
|
581
656
|
for (const id of receiver.flat) flat.add(id);
|
|
582
657
|
}
|
|
658
|
+
let ai = 0;
|
|
583
659
|
for (const arg of expr.args ?? []) {
|
|
584
|
-
const r = resolveExprIdentities(state, arg, ctx);
|
|
660
|
+
const r = resolveExprIdentities(state, arg, sub(ctx, `a${ai}`));
|
|
661
|
+
ai += 1;
|
|
585
662
|
for (const id of r.flat) flat.add(id);
|
|
586
663
|
}
|
|
587
664
|
// Path provenance (§10.1 `call` unresolved row): an unresolved call's
|
|
@@ -608,7 +685,7 @@ export function resolveExprIdentities(state, expr, ctx) {
|
|
|
608
685
|
// A simple pass-through of whatever the assignment's source resolves
|
|
609
686
|
// to (structure-preserving), so its byPath is forwarded directly, not
|
|
610
687
|
// dropped. See DESIGN_INTRAPROCEDURAL.md §4.
|
|
611
|
-
const r = resolveExprIdentities(state, expr.source, ctx);
|
|
688
|
+
const r = resolveExprIdentities(state, expr.source, sub(ctx, 'v'));
|
|
612
689
|
// Path provenance (§10.1 `assign-expr` row): pure pass-through — the
|
|
613
690
|
// source's own recursion already emitted its own in-halves; this hop
|
|
614
691
|
// forwards the source's `widened` flag, so it forwards an approximate
|
|
@@ -694,7 +771,7 @@ function step(node, stateIn, widenings, ctx) {
|
|
|
694
771
|
// `ctx?.recordHop` — mirroring Decision 1's own "extra computation
|
|
695
772
|
// is fine to skip, but only when it is genuinely side-effect-free"
|
|
696
773
|
// intent, which this call never actually satisfied.
|
|
697
|
-
const resolved = resolveExprIdentities(stateIn, node.source, ctx);
|
|
774
|
+
const resolved = resolveExprIdentities(stateIn, node.source, sub(ctx, 'src'));
|
|
698
775
|
if (ctx?.recordHop) {
|
|
699
776
|
const allIds = new Set([...residualFlat(resolved.flat, resolved.byPath), ...[...resolved.byPath.values()].flatMap((s) => [...s])]);
|
|
700
777
|
for (const id of allIds) {
|
|
@@ -707,7 +784,7 @@ function step(node, stateIn, widenings, ctx) {
|
|
|
707
784
|
}
|
|
708
785
|
return { state: stateIn, returnFact: null };
|
|
709
786
|
}
|
|
710
|
-
const resolved = resolveExprIdentities(stateIn, node.source, ctx);
|
|
787
|
+
const resolved = resolveExprIdentities(stateIn, node.source, sub(ctx, 'src'));
|
|
711
788
|
if (pathHasWildcard(node.target)) {
|
|
712
789
|
// A computed-key write (`obj[k] = ...`) must be a WEAK update (add to
|
|
713
790
|
// whatever the container already carries, never clear it first) — a
|
|
@@ -739,6 +816,7 @@ function step(node, stateIn, widenings, ctx) {
|
|
|
739
816
|
kind: 'write-out', subKind: 'assign-weak',
|
|
740
817
|
fromPath: null, toPath: containerPath, dataElementId: id,
|
|
741
818
|
syntacticPath: node.target, widenReason: 'dynamic-property-key', lossReason: null,
|
|
819
|
+
...outStamp(ctx, node.source, '', '/src'),
|
|
742
820
|
});
|
|
743
821
|
}
|
|
744
822
|
}
|
|
@@ -783,6 +861,7 @@ function step(node, stateIn, widenings, ctx) {
|
|
|
783
861
|
kind: 'write-out', subKind: 'assign',
|
|
784
862
|
fromPath: null, toPath: node.target, dataElementId: id,
|
|
785
863
|
syntacticPath: null, widenReason: assignWidenReason, lossReason: null,
|
|
864
|
+
...outStamp(ctx, node.source, '', '/src'),
|
|
786
865
|
});
|
|
787
866
|
}
|
|
788
867
|
}
|
|
@@ -795,6 +874,7 @@ function step(node, stateIn, widenings, ctx) {
|
|
|
795
874
|
kind: 'write-out', subKind: 'assign',
|
|
796
875
|
fromPath: null, toPath, dataElementId: id,
|
|
797
876
|
syntacticPath: null, widenReason: assignWidenReason, lossReason: null,
|
|
877
|
+
...outStamp(ctx, node.source, subPath, '/src'),
|
|
798
878
|
});
|
|
799
879
|
}
|
|
800
880
|
}
|
|
@@ -819,8 +899,10 @@ function step(node, stateIn, widenings, ctx) {
|
|
|
819
899
|
// itself does with the result (still only checks `r.flat` for a
|
|
820
900
|
// widening event) — it only lets a nested `call` sub-expression
|
|
821
901
|
// resolve precisely when `ctx` makes that possible.
|
|
902
|
+
let argIdx = -1;
|
|
822
903
|
for (const arg of node.args ?? []) {
|
|
823
|
-
|
|
904
|
+
argIdx += 1;
|
|
905
|
+
const r = resolveExprIdentities(stateIn, arg, sub(ctx, `a${argIdx}`));
|
|
824
906
|
if (r.flat.size > 0) {
|
|
825
907
|
widenings.push({ atPath: null, dataElementIds: [...r.flat], reason: 'unresolved-call-arg', line: node.line });
|
|
826
908
|
}
|
|
@@ -836,6 +918,7 @@ function step(node, stateIn, widenings, ctx) {
|
|
|
836
918
|
kind: 'write-out', subKind: 'call-arg',
|
|
837
919
|
fromPath: null, toPath: null, dataElementId: id,
|
|
838
920
|
syntacticPath: null, widenReason: null, lossReason: null,
|
|
921
|
+
...(isV2(ctx) ? { derivationScope: `/a${argIdx}`, relPath: '' } : null),
|
|
839
922
|
});
|
|
840
923
|
}
|
|
841
924
|
}
|
|
@@ -844,7 +927,7 @@ function step(node, stateIn, widenings, ctx) {
|
|
|
844
927
|
}
|
|
845
928
|
|
|
846
929
|
case 'return': {
|
|
847
|
-
const resolved = node.value ? resolveExprIdentities(stateIn, node.value, ctx) : { flat: new Set(), widened: false };
|
|
930
|
+
const resolved = node.value ? resolveExprIdentities(stateIn, node.value, sub(ctx, 'val')) : { flat: new Set(), widened: false };
|
|
848
931
|
if (resolved.widened && resolved.flat.size > 0) {
|
|
849
932
|
widenings.push({ atPath: null, dataElementIds: [...resolved.flat], reason: 'unresolved-call', line: node.line });
|
|
850
933
|
}
|
|
@@ -863,6 +946,7 @@ function step(node, stateIn, widenings, ctx) {
|
|
|
863
946
|
kind: 'write-out', subKind: 'return',
|
|
864
947
|
fromPath: null, toPath: null, dataElementId: id,
|
|
865
948
|
syntacticPath: null, widenReason: returnWidenReason, lossReason: null,
|
|
949
|
+
...(isV2(ctx) ? { derivationScope: '/val', relPath: '' } : null),
|
|
866
950
|
});
|
|
867
951
|
}
|
|
868
952
|
}
|
|
@@ -892,6 +976,26 @@ function step(node, stateIn, widenings, ctx) {
|
|
|
892
976
|
// not a load-bearing part of the termination proof.
|
|
893
977
|
const ITER_BUDGET = 5000;
|
|
894
978
|
|
|
979
|
+
// Version 2 labels for one hop: the definitions reaching a read, and the
|
|
980
|
+
// definition a local write creates. A write into another function's
|
|
981
|
+
// namespace (a call-arg-bind) is labelled by the resolver instead.
|
|
982
|
+
function versionStamp(reaching, nid, h, entryState) {
|
|
983
|
+
const out = {};
|
|
984
|
+
if (typeof h.fromPath === 'string') {
|
|
985
|
+
let vs = reaching.versionsAt(nid, h.fromPath);
|
|
986
|
+
// The entry value is an alternative only if the entry state really
|
|
987
|
+
// carried this id at this path; a merge with an unwritten branch must
|
|
988
|
+
// not invent an origin for it.
|
|
989
|
+
if (vs.includes(ENTRY_VERSION) && !entryState.get(h.fromPath)?.has(h.dataElementId)) {
|
|
990
|
+
const rest = vs.filter((v) => v !== ENTRY_VERSION);
|
|
991
|
+
if (rest.length > 0) vs = rest;
|
|
992
|
+
}
|
|
993
|
+
out.fromVersions = vs;
|
|
994
|
+
}
|
|
995
|
+
if (h.kind === 'write-out' && typeof h.toPath === 'string' && h.peerScope == null) out.toVersion = nid;
|
|
996
|
+
return out;
|
|
997
|
+
}
|
|
998
|
+
|
|
895
999
|
export function analyzeFunctionFieldIdentity(fn, entryState, ctx) {
|
|
896
1000
|
const nodes = fn.cfg.nodes;
|
|
897
1001
|
const work = [fn.cfg.entry];
|
|
@@ -900,6 +1004,16 @@ export function analyzeFunctionFieldIdentity(fn, entryState, ctx) {
|
|
|
900
1004
|
const widenings = [];
|
|
901
1005
|
const returnFactsByNode = new Map();
|
|
902
1006
|
let iterations = 0;
|
|
1007
|
+
// Completion record (DAG-07 R07.1): a budget-exhausted run used to fall
|
|
1008
|
+
// out of the loop with a bare `break` and return an empty exit state that
|
|
1009
|
+
// was indistinguishable from "analyzed, nothing flows". `maxIterations`
|
|
1010
|
+
// is overridable through ctx so the limit is testable without building a
|
|
1011
|
+
// 5,000-node CFG; a non-integer or non-positive override is ignored.
|
|
1012
|
+
const maxIterations = Number.isInteger(ctx?.maxIterations) && ctx.maxIterations > 0
|
|
1013
|
+
? ctx.maxIterations : ITER_BUDGET;
|
|
1014
|
+
let budgetExhausted = false;
|
|
1015
|
+
let cancelled = false;
|
|
1016
|
+
const shouldStop = typeof ctx?.shouldStop === 'function' ? ctx.shouldStop : null;
|
|
903
1017
|
// Path provenance (Sub-project C, increment 1, Decision 7.2): stamped
|
|
904
1018
|
// once for the whole analysis, since resolveExprIdentities never sees the
|
|
905
1019
|
// enclosing function.
|
|
@@ -912,9 +1026,17 @@ export function analyzeFunctionFieldIdentity(fn, entryState, ctx) {
|
|
|
912
1026
|
// two hops share a `context` iff the cache would consider them the same
|
|
913
1027
|
// context.
|
|
914
1028
|
const context = ctx?.recordHop ? hashState(entryState) : null;
|
|
1029
|
+
// Correlation version 2 (DAG-03): one reaching-definitions pass per
|
|
1030
|
+
// analysis run; hops are labelled with the definitions their read paths
|
|
1031
|
+
// see and the definition their write creates.
|
|
1032
|
+
const v2 = ctx?.correlationVersion === CORRELATION_VERSION_2 && typeof ctx.recordHop === 'function';
|
|
1033
|
+
const reaching = v2 ? computeReachingDefinitions(fn) : null;
|
|
915
1034
|
|
|
916
1035
|
while (work.length) {
|
|
917
|
-
if (++iterations >
|
|
1036
|
+
if (++iterations > maxIterations) { budgetExhausted = true; break; }
|
|
1037
|
+
// R07.7: cooperative cancellation, polled every 64 iterations so a
|
|
1038
|
+
// deadline costs next to nothing on the common path.
|
|
1039
|
+
if (shouldStop && (iterations & 63) === 1 && shouldStop()) { cancelled = true; break; }
|
|
918
1040
|
const nid = work.shift();
|
|
919
1041
|
const node = nodes[nid];
|
|
920
1042
|
if (!node) continue;
|
|
@@ -927,12 +1049,24 @@ export function analyzeFunctionFieldIdentity(fn, entryState, ctx) {
|
|
|
927
1049
|
// both). With no recorder present, `stepCtx` is the SAME `ctx` reference
|
|
928
1050
|
// — no allocation, nothing for a backward-compatibility test to catch
|
|
929
1051
|
// (Decision 7.2's "true by construction" point).
|
|
930
|
-
const
|
|
931
|
-
?
|
|
1052
|
+
const stepRecord = ctx?.recordHop
|
|
1053
|
+
? (h) => ctx.recordHop({
|
|
932
1054
|
scope, nodeId: nid, line: node.line ?? null,
|
|
933
1055
|
context, peerScope: null, peerContext: null,
|
|
1056
|
+
...(v2 ? versionStamp(reaching, nid, h, entryState) : null),
|
|
934
1057
|
...h,
|
|
935
|
-
})
|
|
1058
|
+
})
|
|
1059
|
+
: null;
|
|
1060
|
+
// `rawRecordHop` (version 2 only) is this level's recorder BELOW the
|
|
1061
|
+
// per-expression derivation wrappers, which is what a callee's analysis
|
|
1062
|
+
// must be handed: the callee's hops carry their own derivation ids and
|
|
1063
|
+
// must not inherit the caller's expression-tree stamps.
|
|
1064
|
+
const stepCtx = stepRecord
|
|
1065
|
+
? {
|
|
1066
|
+
...ctx,
|
|
1067
|
+
...(v2 ? { siteInfo: { scope, nodeId: nid, context }, rawRecordHop: stepRecord } : null),
|
|
1068
|
+
recordHop: stepRecord,
|
|
1069
|
+
}
|
|
936
1070
|
: ctx;
|
|
937
1071
|
const { state: out, returnFact } = step(node, incoming, widenings, stepCtx);
|
|
938
1072
|
if (returnFact && returnFact.size > 0) {
|
|
@@ -988,5 +1122,16 @@ export function analyzeFunctionFieldIdentity(fn, entryState, ctx) {
|
|
|
988
1122
|
if (ids.size > 0) mutatedParams.set(param, ids);
|
|
989
1123
|
}
|
|
990
1124
|
|
|
991
|
-
|
|
1125
|
+
const completion = (budgetExhausted || cancelled)
|
|
1126
|
+
? {
|
|
1127
|
+
status: cancelled ? 'cancelled' : 'partial',
|
|
1128
|
+
reasons: [cancelled ? 'cancelled' : 'analysis-iteration-budget'],
|
|
1129
|
+
iterations: maxIterations,
|
|
1130
|
+
maxIterations,
|
|
1131
|
+
unfinishedWork: work.length,
|
|
1132
|
+
exitReached: outStates.has(fn.cfg.exit),
|
|
1133
|
+
}
|
|
1134
|
+
: { status: 'complete', reasons: [], iterations, maxIterations, unfinishedWork: 0, exitReached: outStates.has(fn.cfg.exit) };
|
|
1135
|
+
|
|
1136
|
+
return { exitState, returnFacts, mutatedParams, widenings, completion };
|
|
992
1137
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// (the `agentic-security scan` and `agentic-security ci` commands). This
|
|
15
15
|
// module computes its own content digest instead of reusing either.
|
|
16
16
|
|
|
17
|
+
import { completionOfGraph } from './completion.js';
|
|
17
18
|
import * as crypto from 'node:crypto';
|
|
18
19
|
import { _redactGraph } from './redact-graph.js';
|
|
19
20
|
|
|
@@ -232,6 +233,10 @@ export function exportGraphJSON(graph, opts = {}) {
|
|
|
232
233
|
scope: graph?.scope ?? null,
|
|
233
234
|
coverage: graph?.coverage ?? null,
|
|
234
235
|
limitations: graph?.limitations ?? [],
|
|
236
|
+
// DAG-07 R07.2: the completion envelope, derived from the source graph
|
|
237
|
+
// so it is the same for any filter/redaction of one scan. Top-level and
|
|
238
|
+
// additive: the digests above cover `graph`, which is unchanged.
|
|
239
|
+
completion: completionOfGraph(graph),
|
|
235
240
|
confidential: true,
|
|
236
241
|
graph: body,
|
|
237
242
|
};
|
|
@@ -133,6 +133,8 @@ import { emptyProtection, aggregateVerdicts } from './protection.js';
|
|
|
133
133
|
import * as ids from './ids.js';
|
|
134
134
|
import { planSeeds, seedEntryStateFactory, exprRoots, walkExpr } from './source-seeding.js';
|
|
135
135
|
import { RECIPIENT_FACT_FIELDS } from './recipient-profile.js';
|
|
136
|
+
import { pathEvidenceEnabled, evaluatePathEvidence, summarizeFlowEvidence } from './path-evidence.js';
|
|
137
|
+
import { sinkFamilyOf } from './control-credit.js';
|
|
136
138
|
|
|
137
139
|
// =========================================================================
|
|
138
140
|
// §4.2 / §4.3 — registry-backed sink enumeration, multi-candidate resolution
|
|
@@ -438,16 +440,46 @@ const nodeLabel = (kind, category, reason) => (category
|
|
|
438
440
|
*/
|
|
439
441
|
export function buildDataFlowGraph(callGraph, opts = {}) {
|
|
440
442
|
const repository = opts.repository ?? 'repo';
|
|
441
|
-
const { seeds, unseedable } = planSeeds(callGraph, {
|
|
443
|
+
const { seeds, unseedable, completion: seedingCompletion } = planSeeds(callGraph, {
|
|
444
|
+
repository,
|
|
445
|
+
...(opts.seeding ?? {}),
|
|
446
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
447
|
+
...(opts.deadline === undefined ? {} : { deadline: opts.deadline }),
|
|
448
|
+
});
|
|
442
449
|
const hops = [];
|
|
443
|
-
runFieldIdentityAnalysis(callGraph, {
|
|
450
|
+
const analysis = runFieldIdentityAnalysis(callGraph, {
|
|
444
451
|
recordHop: (h) => hops.push(h),
|
|
445
452
|
seedEntryState: seedEntryStateFactory(seeds),
|
|
446
453
|
...(opts.maxContextsPerFn === undefined ? {} : { maxContextsPerFn: opts.maxContextsPerFn }),
|
|
454
|
+
...(opts.maxIterations === undefined ? {} : { maxIterations: opts.maxIterations }),
|
|
455
|
+
// DAG-03: provenance correlation version 2 (off unless requested).
|
|
456
|
+
...(opts.correlationVersion === undefined ? {} : { correlationVersion: opts.correlationVersion }),
|
|
457
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
458
|
+
...(opts.deadline === undefined ? {} : { deadline: opts.deadline }),
|
|
447
459
|
...(opts.onProgress ? { onProgress: opts.onProgress } : {}),
|
|
448
460
|
});
|
|
449
|
-
const store = new PathStore();
|
|
461
|
+
const store = new PathStore({}, { correlationVersion: opts.correlationVersion });
|
|
450
462
|
store.addHops(hops);
|
|
463
|
+
// DAG-07 R07.2: an analysis unit that hit its budget has no hop
|
|
464
|
+
// representation (DESIGN_PATH_PROVENANCE.md section 14.8), so it reaches
|
|
465
|
+
// the store through the out-of-band channel built for exactly this. Units
|
|
466
|
+
// that emitted nothing stay visible through `analysisCompletion` below.
|
|
467
|
+
const analysisCompletion = { ...analysis.completion };
|
|
468
|
+
const _storeLimits = store.limitStatus();
|
|
469
|
+
if (_storeLimits.hit) {
|
|
470
|
+
analysisCompletion.storeLimits = _storeLimits;
|
|
471
|
+
if (analysisCompletion.status === 'complete') analysisCompletion.status = 'partial';
|
|
472
|
+
}
|
|
473
|
+
// R07.1: the seeding step's own record. Seeds the planner never reached
|
|
474
|
+
// mean functions whose sources were never looked for.
|
|
475
|
+
if (seedingCompletion.status !== 'complete') {
|
|
476
|
+
analysisCompletion.seeding = seedingCompletion;
|
|
477
|
+
if (seedingCompletion.status === 'cancelled') analysisCompletion.status = 'cancelled';
|
|
478
|
+
else if (analysisCompletion.status === 'complete') analysisCompletion.status = 'partial';
|
|
479
|
+
}
|
|
480
|
+
for (const u of analysisCompletion.incomplete) {
|
|
481
|
+
store.markTruncated(u.qid, u.context, u.reasons[0] ?? 'analysis-incomplete');
|
|
482
|
+
}
|
|
451
483
|
|
|
452
484
|
const cfgByQid = new Map();
|
|
453
485
|
for (const fn of callGraph.functions.values()) cfgByQid.set(fn.qid, { fn, nodes: fn.cfg?.nodes ?? {} });
|
|
@@ -740,7 +772,7 @@ export function buildDataFlowGraph(callGraph, opts = {}) {
|
|
|
740
772
|
// key by design and therefore never splits a flow.
|
|
741
773
|
const flowKey = [src.id, snk.id, de.id, p.shape, g.grade, sortedT.join(',')].join('|');
|
|
742
774
|
if (!groupsByFlowKey.has(flowKey)) groupsByFlowKey.set(flowKey, []);
|
|
743
|
-
groupsByFlowKey.get(flowKey).push({ p, g, src, snk, de, edgeIdStr, sortedT, site, unattributed, truncated: r.truncated, truncationReasons: r.truncationReasons });
|
|
775
|
+
groupsByFlowKey.get(flowKey).push({ p, g, src, snk, de, edgeIdStr, sortedT, site, unattributed, truncated: r.truncated, truncationReasons: r.truncationReasons, result: r });
|
|
744
776
|
}
|
|
745
777
|
}
|
|
746
778
|
site.connected = connected;
|
|
@@ -750,6 +782,22 @@ export function buildDataFlowGraph(callGraph, opts = {}) {
|
|
|
750
782
|
sinkNodeFor(site);
|
|
751
783
|
}
|
|
752
784
|
|
|
785
|
+
// DAG-04: opt-in path predicates and control credit (see path-evidence.js).
|
|
786
|
+
const pathEvidenceOn = pathEvidenceEnabled(opts);
|
|
787
|
+
const _fnOf = (q) => cfgByQid.get(q)?.fn ?? null;
|
|
788
|
+
const pathEvidenceFor = (group) => {
|
|
789
|
+
const paths = group.map((x) => x.p);
|
|
790
|
+
const family = sinkFamilyOf(group[0].site.entry);
|
|
791
|
+
const evidences = paths.map((p) => evaluatePathEvidence({ path: p, nodeOf: (id) => store.getNode(id), fnOf: _fnOf, sinkFamily: family }));
|
|
792
|
+
const results = [...new Set(group.map((x) => x.result))];
|
|
793
|
+
const merged = {
|
|
794
|
+
status: results.every((x) => x.status === 'complete') ? 'complete' : 'partial',
|
|
795
|
+
truncated: results.some((x) => x.truncated), droppedPathCount: results.reduce((n, x) => n + (x.droppedPathCount ?? 0), 0),
|
|
796
|
+
analysisTruncated: results.some((x) => x.analysisTruncated), resumed: results.some((x) => x.resumed),
|
|
797
|
+
};
|
|
798
|
+
return summarizeFlowEvidence({ result: merged, paths, evidences });
|
|
799
|
+
};
|
|
800
|
+
|
|
753
801
|
for (const [, group] of [...groupsByFlowKey.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
|
|
754
802
|
const { p, g, src, snk, de, edgeIdStr, sortedT, site } = group[0];
|
|
755
803
|
const fId = ids.flowId(src.id, snk.id, [de.id], [p.shape, g.grade, sortedT.join(',')]);
|
|
@@ -927,7 +975,16 @@ export function buildDataFlowGraph(callGraph, opts = {}) {
|
|
|
927
975
|
limitations,
|
|
928
976
|
evidenceGrade: g.grade,
|
|
929
977
|
handling: handlingResult,
|
|
978
|
+
// DAG-04 (default OFF; fields exist only when enabled, so default graphs and digests do not move):
|
|
979
|
+
// per-path branch conditions, feasibility under the bounded model, and control credit for the sink
|
|
980
|
+
// family. Alternatives stay distinct; infeasible-under-model paths are listed, not dropped.
|
|
981
|
+
...(pathEvidenceOn ? { pathEvidence: pathEvidenceFor(group) } : {}),
|
|
930
982
|
});
|
|
983
|
+
if (pathEvidenceOn) {
|
|
984
|
+
const fe = edgesById.get(edgeIdStr);
|
|
985
|
+
const refs = [...new Set(flowsById.get(fId).pathEvidence.alternatives.flatMap((a) => a.predicateRefs.map((x) => x.ref)))].sort();
|
|
986
|
+
if (fe && refs.length) fe.predicateRefs = [...new Set([...(fe.predicateRefs ?? []), ...refs])].sort();
|
|
987
|
+
}
|
|
931
988
|
}
|
|
932
989
|
|
|
933
990
|
// Milestone 2, Sub-project D, increment 2 (FR-307): appliesToAllPaths.
|
|
@@ -1105,15 +1162,35 @@ export function buildDataFlowGraph(callGraph, opts = {}) {
|
|
|
1105
1162
|
},
|
|
1106
1163
|
degradedTerminals: degraded.length,
|
|
1107
1164
|
paths: { enumerated: stats.pathsEnumerated, projected: stats.pathsProjected, truncatedQueries: stats.truncatedQueries },
|
|
1108
|
-
provenance: {
|
|
1165
|
+
provenance: {
|
|
1166
|
+
hops: hops.length, pnodes: store.stats().nodes, pedges: store.stats().edges,
|
|
1167
|
+
...(store.correlationVersion() === 2 ? { correlationVersion: 2 } : null),
|
|
1168
|
+
},
|
|
1169
|
+
...(analysisCompletion.status === 'complete' ? {} : { analysisCompletion }),
|
|
1109
1170
|
};
|
|
1110
1171
|
graph.limitations = [
|
|
1111
1172
|
'Protection verdicts are not assessed in Milestone 1: every edge carries not_assessed/none on all three dimensions.',
|
|
1112
1173
|
'Policy verdicts are not evaluated in Milestone 1.',
|
|
1113
1174
|
'External destinations are not resolved (FR-202 is Milestone 2); protocol.destinationResolution is "unknown" on every edge.',
|
|
1114
1175
|
];
|
|
1115
|
-
graph.scanHealth =
|
|
1176
|
+
graph.scanHealth = analysisCompletion.status === 'complete'
|
|
1177
|
+
? { status: 'complete', reason: 'lineage-graph-builder', analysisCompletion }
|
|
1178
|
+
: {
|
|
1179
|
+
status: analysisCompletion.status === 'cancelled' ? 'cancelled' : 'partial',
|
|
1180
|
+
reason: analysisCompletion.status === 'cancelled' ? 'analysis-cancelled' : 'analysis-incomplete',
|
|
1181
|
+
analysisCompletion,
|
|
1182
|
+
};
|
|
1183
|
+
if (analysisCompletion.status !== 'complete') {
|
|
1184
|
+
const parts = [];
|
|
1185
|
+
if (analysisCompletion.incomplete.length) parts.push(`field analysis stopped early in ${analysisCompletion.incomplete.length} of ${analysisCompletion.functionsAnalyzed} function(s) (iteration budget)`);
|
|
1186
|
+
if (analysisCompletion.status === 'cancelled') parts.push(`analysis was cancelled; ${analysisCompletion.functionsNotAnalyzed} function(s) were never analyzed`);
|
|
1187
|
+
if (analysisCompletion.storeLimits) parts.push('provenance construction limits were reached, so some flows were not materialized');
|
|
1188
|
+
if (analysisCompletion.seeding) parts.push(`source seeding stopped early (${analysisCompletion.seeding.reasons.join(', ')}); ${analysisCompletion.seeding.functionsNotScanned} function(s) were not searched for sources`);
|
|
1189
|
+
if (analysisCompletion.incremental) parts.push('a cached summary stood in for a tainted entry context');
|
|
1190
|
+
if (analysisCompletion.degraded.length) parts.push(`${analysisCompletion.degraded.length} call context(s) fell back to a coarser summary (context cap or unconverged recursion)`);
|
|
1191
|
+
graph.limitations.push(`${parts.join('; ')}; absence of a flow from those functions is not evidence that none exists.`);
|
|
1192
|
+
}
|
|
1116
1193
|
graph.taxonomy = { version: '1.0.0', source: 'built-in + CONFIDENTIAL extension' };
|
|
1117
1194
|
|
|
1118
|
-
return { graph, store, hops, seeds, unseedable, sites, nonStatementSites, degraded, stats, decisionsByNodeId };
|
|
1195
|
+
return { graph, store, hops, analysisCompletion, seeds, unseedable, sites, nonStatementSites, degraded, stats, decisionsByNodeId };
|
|
1119
1196
|
}
|
|
@@ -317,3 +317,62 @@ export function adaptCallGraphForLineage(callGraph) {
|
|
|
317
317
|
const resolveKnownCallee = callGraph.resolveKnownCallee ? (...a) => swap(callGraph.resolveKnownCallee(...a)) : undefined;
|
|
318
318
|
return { ...callGraph, functions, ...(resolve ? { resolve } : {}), ...(resolveKnownCallee ? { resolveKnownCallee } : {}), lineageView: { haskell: true, functions: clones.size } };
|
|
319
319
|
}
|
|
320
|
+
|
|
321
|
+
// ── DAG-08: boundary view (record and monadic bindings, unresolved higher-order constructs) ──────────────────────────────
|
|
322
|
+
// A boundary contract that names a Haskell consumer (a function and one of its parameters) is bound by
|
|
323
|
+
// lineage/boundary-contract.js against the same parser-resolved functions this view clones. These helpers add what a consumer
|
|
324
|
+
// binding needs from this layer and must not hide:
|
|
325
|
+
// * the ORIGINAL source location of the bound function next to the lineage-view clone's location (both kept, neither invented);
|
|
326
|
+
// * every higher-order construct (`mapM_ f xs`, `forM_ xs k`, `traverse_`, `>>=` with a function value) whose function
|
|
327
|
+
// argument the Haskell resolver could not resolve to a project function. Taint through such a construct is not followed, so it
|
|
328
|
+
// is listed instead of being treated as resolved.
|
|
329
|
+
const HIGHER_ORDER_VIA = /^(?:mapM_?|forM_?|traverse_?|for_|foldM_?|foldr|foldl'?|fmap|<\$>|>>=|=<<|bracket_?|handle|catch|forever|when|unless|zipWithM_?)$/;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @param {object} callGraph the Haskell call graph (original or lineage-adapted)
|
|
333
|
+
* @returns {{functions: object[], unresolvedHigherOrder: object[], notInlined: object[]}}
|
|
334
|
+
*/
|
|
335
|
+
export function describeHaskellBoundaries(callGraph) {
|
|
336
|
+
const out = { functions: [], unresolvedHigherOrder: [], notInlined: [] };
|
|
337
|
+
if (!callGraph || typeof callGraph.functions?.values !== 'function') return out;
|
|
338
|
+
for (const fn of callGraph.functions.values()) {
|
|
339
|
+
if (!isHsFile(fn.file) || !fn.cfg) continue;
|
|
340
|
+
out.functions.push({
|
|
341
|
+
qid: fn.qid, name: fn.name, file: fn.file, line: fn.line,
|
|
342
|
+
view: fn.hs && fn.hs.lineageView ? { adapted: true, inlinedCalls: fn.hs.lineageInlinedCalls || 0 } : { adapted: false },
|
|
343
|
+
recordParams: ((fn.hs && fn.hs.recordParams) || []).map((r) => ({ name: r.name, type: r.type, fields: [...(r.fields || [])] })),
|
|
344
|
+
});
|
|
345
|
+
for (const s of (fn.hs && fn.hs.lineageNotInlined) || []) out.notInlined.push({ function: fn.qid, file: fn.file, line: fn.line, callee: s.callee, reason: s.reason });
|
|
346
|
+
for (const n of Object.values(fn.cfg.nodes)) {
|
|
347
|
+
const call = n.kind === 'call' ? n : (n.kind === 'assign' && n.source && n.source.kind === 'call' ? n.source : null);
|
|
348
|
+
if (!call || !call.hs) continue;
|
|
349
|
+
const via = call.hs.via;
|
|
350
|
+
if (!via || !HIGHER_ORDER_VIA.test(via)) continue;
|
|
351
|
+
if (call.hs.status === 'resolved' && call.hs.target) continue;
|
|
352
|
+
out.unresolvedHigherOrder.push({
|
|
353
|
+
function: fn.qid, file: fn.file, line: n.line ?? call.line ?? fn.line, via, status: call.hs.status || 'unknown',
|
|
354
|
+
callee: typeof call.callee === 'string' ? call.callee : null,
|
|
355
|
+
reason: call.hs.status === 'param' ? 'the function argument is a parameter of the enclosing function' : 'the function argument was not resolved to a project function',
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const key = (a) => `${a.file}:${String(a.line).padStart(8, '0')}:${a.via}`;
|
|
360
|
+
out.unresolvedHigherOrder.sort((a, b) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0));
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The original and lineage-view locations of a bound Haskell consumer. `adapted` is the result of adaptCallGraphForLineage.
|
|
366
|
+
* Both refer to the same source file and line: the view clones the function but never moves or renames it.
|
|
367
|
+
*/
|
|
368
|
+
export function haskellConsumerLocations(adapted, binding) {
|
|
369
|
+
if (!binding || !adapted || typeof adapted.functions?.get !== 'function') return null;
|
|
370
|
+
const clone = adapted.functions.get(binding.qid);
|
|
371
|
+
if (!clone) return null;
|
|
372
|
+
const orig = binding.originalLocation || { file: binding.file, line: binding.line };
|
|
373
|
+
return {
|
|
374
|
+
original: { file: orig.file, line: orig.line },
|
|
375
|
+
view: { file: clone.file, line: clone.line, view: 'haskell-lineage', adapted: !!(clone.hs && clone.hs.lineageView), inlinedCalls: (clone.hs && clone.hs.lineageInlinedCalls) || 0 },
|
|
376
|
+
notInlined: (clone.hs && clone.hs.lineageNotInlined) || [],
|
|
377
|
+
};
|
|
378
|
+
}
|
package/src/lineage/index.js
CHANGED
|
@@ -277,6 +277,9 @@ export function buildLineageGraph(callGraph, opts = {}) {
|
|
|
277
277
|
observationWindowStart: opts.observationWindowStart,
|
|
278
278
|
observationWindowEnd: opts.observationWindowEnd,
|
|
279
279
|
onProgress: opts.onProgress,
|
|
280
|
+
// DAG-07 R07.7: cooperative stop, forwarded only when supplied.
|
|
281
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
282
|
+
...(opts.deadline === undefined ? {} : { deadline: opts.deadline }),
|
|
280
283
|
});
|
|
281
284
|
const nixFiles = opts.fileContents && Object.keys(opts.fileContents).some((f) => /\.nix$/i.test(f));
|
|
282
285
|
if (nixFiles) contributeNixConfigLineage(built.graph, opts.fileContents, { repository: opts.repository });
|