@clear-capabilities/agentic-security-scanner 0.145.0 → 0.147.5
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 +266 -0
- package/bin/agentic-security.js +3508 -69
- package/dist/1122.index.js +779 -0
- package/dist/{301.index.js → 1301.index.js} +2 -2
- package/dist/1379.index.js +591 -0
- package/dist/{444.index.js → 1444.index.js} +2 -2
- package/dist/{660.index.js → 1660.index.js} +2 -2
- package/dist/{700.index.js → 1700.index.js} +2 -2
- package/dist/{905.index.js → 1905.index.js} +3 -3
- package/dist/{920.index.js → 1920.index.js} +3 -3
- package/dist/{238.index.js → 2238.index.js} +3 -3
- package/dist/{271.index.js → 2271.index.js} +2 -2
- package/dist/{985.index.js → 2376.index.js} +1260 -340
- package/dist/2432.index.js +793 -0
- package/dist/2659.index.js +93 -0
- package/dist/{826.index.js → 2826.index.js} +2 -2
- package/dist/{830.index.js → 2830.index.js} +2 -2
- package/dist/2923.index.js +298 -0
- package/dist/{1.index.js → 3001.index.js} +5 -5
- package/dist/{117.index.js → 3117.index.js} +3 -3
- package/dist/3180.index.js +379 -0
- package/dist/3276.index.js +117 -0
- package/dist/{415.index.js → 3415.index.js} +2 -2
- package/dist/{499.index.js → 3499.index.js} +2 -2
- package/dist/3518.index.js +450 -0
- package/dist/{526.index.js → 3526.index.js} +5 -5
- package/dist/{736.index.js → 3736.index.js} +4 -4
- package/dist/{839.index.js → 3839.index.js} +4 -4
- package/dist/{113.index.js → 4113.index.js} +5 -5
- package/dist/{265.index.js → 4265.index.js} +2 -2
- package/dist/{384.index.js → 4384.index.js} +3 -3
- package/dist/4547.index.js +268 -0
- package/dist/4863.index.js +422 -0
- package/dist/{970.index.js → 4970.index.js} +2 -2
- package/dist/5051.index.js +841 -0
- package/dist/{144.index.js → 5144.index.js} +5 -5
- package/dist/{333.index.js → 5333.index.js} +3 -3
- package/dist/5343.index.js +185 -0
- package/dist/5350.index.js +866 -0
- package/dist/5561.index.js +436 -0
- package/dist/{637.index.js → 5637.index.js} +3 -3
- package/dist/{449.index.js → 5830.index.js} +2 -2
- package/dist/6626.index.js +532 -0
- package/dist/6662.index.js +297 -0
- package/dist/{675.index.js → 6675.index.js} +5 -5
- package/dist/{730.index.js → 6730.index.js} +6 -6
- package/dist/6829.index.js +225 -0
- package/dist/6944.index.js +130 -0
- package/dist/{178.index.js → 7178.index.js} +3 -3
- package/dist/{227.index.js → 7227.index.js} +2 -2
- package/dist/7310.index.js +520 -0
- package/dist/{552.index.js → 7552.index.js} +4 -4
- package/dist/7709.index.js +78 -0
- package/dist/8218.index.js +160 -0
- package/dist/{476.index.js → 8476.index.js} +4 -4
- package/dist/{513.index.js → 8513.index.js} +5 -5
- package/dist/{520.index.js → 8520.index.js} +2 -2
- package/dist/{718.index.js → 8718.index.js} +2 -2
- package/dist/{752.index.js → 8752.index.js} +2 -2
- package/dist/8846.index.js +100 -0
- package/dist/{435.index.js → 9091.index.js} +560 -189
- package/dist/{207.index.js → 9207.index.js} +2 -2
- package/dist/{220.index.js → 9220.index.js} +2 -2
- package/dist/9390.index.js +163 -0
- package/dist/{503.index.js → 9503.index.js} +2 -2
- package/dist/{801.index.js → 9801.index.js} +2 -2
- package/dist/{824.index.js → 9824.index.js} +2 -2
- package/dist/agentic-security.mjs +16 -16
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/dist/frontend/index.html +21 -0
- package/dist/frontend/src/app.js +176 -0
- package/dist/frontend/src/components/evidence-inspector.js +141 -0
- package/dist/frontend/src/components/filter-rail.js +119 -0
- package/dist/frontend/src/components/query-bar.js +126 -0
- package/dist/frontend/src/data/flagship-graph.js +1460 -0
- package/dist/frontend/src/export-entry.js +36 -0
- package/dist/frontend/src/lib/api-client.js +92 -0
- package/dist/frontend/src/lib/contrast.js +34 -0
- package/dist/frontend/src/lib/dom.js +24 -0
- package/dist/frontend/src/lib/escape-html.js +16 -0
- package/dist/frontend/src/lib/flow-path.js +40 -0
- package/dist/frontend/src/lib/focus-controls.js +149 -0
- package/dist/frontend/src/lib/protection-visual.js +46 -0
- package/dist/frontend/src/lib/query-language.js +240 -0
- package/dist/frontend/src/lib/row-filters.js +43 -0
- package/dist/frontend/src/lib/state.js +84 -0
- package/dist/frontend/src/main.js +83 -0
- package/dist/frontend/src/shell.js +184 -0
- package/dist/frontend/src/views/architecture-view.js +798 -0
- package/dist/frontend/src/views/inventory-view.js +292 -0
- package/dist/frontend/src/views/privacy-view.js +172 -0
- package/dist/frontend/src/views/trace-view.js +206 -0
- package/dist/frontend/styles/architecture-view.css +93 -0
- package/dist/frontend/styles/filter-rail.css +34 -0
- package/dist/frontend/styles/inspector.css +69 -0
- package/dist/frontend/styles/inventory-view.css +74 -0
- package/dist/frontend/styles/privacy-view.css +86 -0
- package/dist/frontend/styles/query-bar.css +107 -0
- package/dist/frontend/styles/shell.css +155 -0
- package/dist/frontend/styles/tokens.css +128 -0
- package/dist/frontend/styles/trace-view.css +95 -0
- package/package.json +13 -6
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +42 -0
- package/src/dataflow/orm-write-catalog.js +175 -0
- package/src/engine.js +92 -1
- package/src/ir/CLAUDE.md +1 -0
- package/src/ir/chrome-probe.mjs +150 -0
- package/src/ir/parser-js.js +94 -7
- package/src/lineage/CLAUDE.md +1203 -0
- package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
- package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
- package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
- package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
- package/src/lineage/DESIGN_REGISTRIES.md +880 -0
- package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
- package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
- package/src/lineage/classification.js +56 -0
- package/src/lineage/coverage.js +658 -0
- package/src/lineage/cross-repo-link.js +107 -0
- package/src/lineage/dataflow-graph.schema.json +184 -0
- package/src/lineage/decision-story.js +206 -0
- package/src/lineage/drift-policy.js +279 -0
- package/src/lineage/driver.js +135 -0
- package/src/lineage/engine.js +992 -0
- package/src/lineage/export-briefing.js +628 -0
- package/src/lineage/export-csv.js +62 -0
- package/src/lineage/export-json.js +238 -0
- package/src/lineage/export-privacy.js +258 -0
- package/src/lineage/federation-loader.js +111 -0
- package/src/lineage/field-identity.js +78 -0
- package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
- package/src/lineage/fixtures/flagship-graph.json +1453 -0
- package/src/lineage/flow-grade.js +221 -0
- package/src/lineage/governance-edit.js +169 -0
- package/src/lineage/graph-builder.js +1114 -0
- package/src/lineage/graph-diff.js +431 -0
- package/src/lineage/graph-snapshot.js +180 -0
- package/src/lineage/handling-analyzer.js +168 -0
- package/src/lineage/ids.js +349 -0
- package/src/lineage/impact-assessment.js +76 -0
- package/src/lineage/impact-engine.js +268 -0
- package/src/lineage/index.js +281 -0
- package/src/lineage/language-coverage-tiers.js +58 -0
- package/src/lineage/obligation-mapping.js +126 -0
- package/src/lineage/obligation-predicates.js +235 -0
- package/src/lineage/observation-adapters.js +282 -0
- package/src/lineage/observation-correlation.js +622 -0
- package/src/lineage/observation-store.js +497 -0
- package/src/lineage/path-query.js +410 -0
- package/src/lineage/path-store.js +400 -0
- package/src/lineage/protection.js +53 -0
- package/src/lineage/recipient-profile.js +192 -0
- package/src/lineage/recipient-registry.js +394 -0
- package/src/lineage/redact-graph.js +224 -0
- package/src/lineage/remediation.js +417 -0
- package/src/lineage/resolve-destination.js +91 -0
- package/src/lineage/runtime-observation.js +464 -0
- package/src/lineage/scenario-diff.js +84 -0
- package/src/lineage/scenario-engine.js +251 -0
- package/src/lineage/scenario.js +101 -0
- package/src/lineage/schema.js +167 -0
- package/src/lineage/sink-registry.js +427 -0
- package/src/lineage/source-registry.js +357 -0
- package/src/lineage/source-seeding.js +212 -0
- package/src/lineage/summaries.js +590 -0
- package/src/lineage/transform-catalog.js +397 -0
- package/src/lineage/transit-protection.js +150 -0
- package/src/lineage/validate.js +285 -0
- package/src/mcp/CLAUDE.md +7 -1
- package/src/mcp/dataflow-tools.js +160 -0
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +2 -1
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/CLAUDE.md +19 -0
- package/src/posture/artifact-registry.js +52 -0
- package/src/posture/auditor-walkthrough.js +76 -0
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/src/posture/obligation-evidence-pack.js +202 -0
- package/src/posture/remediation-ledger.js +337 -0
- package/src/server/CLAUDE.md +47 -0
- package/src/server/graph-loader.js +141 -0
- package/src/server/http-server.js +325 -0
- package/src/server/routes.js +129 -0
- package/src/server/security.js +111 -0
- package/src/server/static-assets.js +144 -0
- package/src/shared/frontend-root.js +52 -0
- package/dist/11.index.js +0 -353
- package/dist/259.index.js +0 -975
- package/dist/317.index.js +0 -300
- package/dist/609.index.js +0 -741
- package/dist/838.index.js +0 -152
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
//
|
|
2
|
+
// source-registry.js — Data Flow Explorer, Sub-project D, increment D2.
|
|
3
|
+
//
|
|
4
|
+
// Reclassifies scanner/src/dataflow/catalog.js's `kind: 'source'` entries
|
|
5
|
+
// (180 as of this writing — re-derive via `CATALOG.filter(e =>
|
|
6
|
+
// e.kind === 'source').length` rather than trusting this comment; the
|
|
7
|
+
// dataflow/CLAUDE.md count has already drifted twice) into DataFlowGraph
|
|
8
|
+
// v1's `SOURCE_CATEGORIES` / `coverageStatus` vocabulary (`schema.js`).
|
|
9
|
+
//
|
|
10
|
+
// This module is the MECHANICAL implementation of a binding, independently
|
|
11
|
+
// reviewed design — it is not a fresh design decision. Every mapping row
|
|
12
|
+
// below traces to `DESIGN_REGISTRIES.md`:
|
|
13
|
+
// §4 — PROVENANCE_MAP, the primary mapping (keyed on the entry's
|
|
14
|
+
// declared `provenance` field).
|
|
15
|
+
// §4.1 — AGENT_TOOL_REFINEMENT: `agent-tool` provenance splits per-entry
|
|
16
|
+
// between model-produced tool arguments and results/resources
|
|
17
|
+
// flowing back to the model.
|
|
18
|
+
// §4.2 — the `language === 'cpp'` descriptor-generic-I/O refinement:
|
|
19
|
+
// `recv`/`recvfrom`/`read`/`fread`/`fgets` cannot say whether
|
|
20
|
+
// they read a file, socket, pipe or stdin, so their C entries
|
|
21
|
+
// demote to `partial`.
|
|
22
|
+
// §4.3 — NO_PROVENANCE_OVERRIDES: the 82 entries with no declared
|
|
23
|
+
// `provenance` at all, resolved by a per-entry-id override table.
|
|
24
|
+
// Per §9.1, THIS module is the table's permanent home.
|
|
25
|
+
// §6 — the `coverageStatus` decision procedure (modeled/partial/
|
|
26
|
+
// candidate/unsupported — never `manual`, §6.5).
|
|
27
|
+
// §7.1 — node kind is always `'source'` for a reclassified source entry
|
|
28
|
+
// (checked by this module's own tests, not assumed).
|
|
29
|
+
// §9.0 — the shared `{kind, category, coverageStatus, externality,
|
|
30
|
+
// reason}` decision shape.
|
|
31
|
+
//
|
|
32
|
+
// Pure reclassification layer: reads CATALOG entries as DATA and maps them
|
|
33
|
+
// onto the target vocabulary. It never re-derives what a call site
|
|
34
|
+
// matches — `matchSource()` in dataflow/catalog.js already owns that, and
|
|
35
|
+
// duplicating it here would fork a matcher the corpus proves against one
|
|
36
|
+
// that nothing proves (DESIGN_REGISTRIES.md §1).
|
|
37
|
+
//
|
|
38
|
+
// Isolation: imports ONLY `CATALOG` from ../dataflow/catalog.js. Never
|
|
39
|
+
// dataflow/engine.js, never dataflow/summaries.js, never matcher internals
|
|
40
|
+
// (this PRD's §18.1 isolation principle, same basis Sub-project A/B/C use
|
|
41
|
+
// for their own `dataflow/` reuse).
|
|
42
|
+
//
|
|
43
|
+
// The field this module emits is `category` — NOT `subtype`. On a
|
|
44
|
+
// DataFlowGraph v1 node, `category` becomes `node.subtype` (§9.0). A
|
|
45
|
+
// registry must never itself emit a field literally called `subtype`: the
|
|
46
|
+
// two are the same vocabulary at different layers, and conflating them
|
|
47
|
+
// would let a registry decision validate as a real graph node without ever
|
|
48
|
+
// passing through Sub-project E's future graph builder.
|
|
49
|
+
|
|
50
|
+
import { CATALOG } from '../dataflow/catalog.js';
|
|
51
|
+
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
53
|
+
// §4 — PROVENANCE_MAP: the primary source-side mapping, keyed on the
|
|
54
|
+
// entry's declared `provenance` field (12 rows).
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export const PROVENANCE_MAP = Object.freeze({
|
|
58
|
+
'http-body': Object.freeze({ category: 'http-body', status: 'modeled', why: 'exact vocabulary match' }),
|
|
59
|
+
'url-param': Object.freeze({ category: 'http-query', status: 'modeled', why: 'pure rename; the query string' }),
|
|
60
|
+
'path-param': Object.freeze({ category: 'http-route', status: 'modeled', why: 'pure rename; a route/path segment' }),
|
|
61
|
+
'header': Object.freeze({ category: 'http-header', status: 'modeled', why: 'pure rename' }),
|
|
62
|
+
'cookie': Object.freeze({ category: 'http-cookie', status: 'modeled', why: 'pure rename' }),
|
|
63
|
+
'env': Object.freeze({ category: 'env-value', status: 'modeled', why: 'pure rename' }),
|
|
64
|
+
'cli': Object.freeze({ category: 'cli-argument', status: 'modeled', why: 'pure rename' }),
|
|
65
|
+
// Both rows below carry a `language === 'cpp'` refinement (§4.2) — C's
|
|
66
|
+
// I/O primitives are descriptor-generic, so their C entries demote to
|
|
67
|
+
// `partial` (see CPP_DESCRIPTOR_GENERIC_PROVENANCE below).
|
|
68
|
+
'network': Object.freeze({ category: 'external-api-response', status: 'modeled', why: 'reads an outbound response body (fetch/axios/requests/urlopen)' }),
|
|
69
|
+
'file-read': Object.freeze({ category: 'storage-read', status: 'modeled', why: "FR-101 groups 'files and object storage reads'; storage-read is its only encoding" }),
|
|
70
|
+
'url-fragment': Object.freeze({ category: 'http-query', status: 'partial', why: 'LOSSY: a fragment is URL-borne but never transmitted to the server; SOURCE_CATEGORIES has no http-fragment value' }),
|
|
71
|
+
'stdin': Object.freeze({ category: 'user-input', status: 'partial', why: 'LOSSY: broadens to the generic category; no stdin/console value exists' }),
|
|
72
|
+
// `agent-tool` is the one provenance value that does NOT resolve on the
|
|
73
|
+
// provenance key alone — its 8 entries split directionally between
|
|
74
|
+
// model-produced tool ARGUMENTS and tool RESULTS/resources flowing back
|
|
75
|
+
// (§4.1). This row exists so the completeness guard still sees the key
|
|
76
|
+
// covered, and records WHY it needs refinement. `status: 'split'` is an
|
|
77
|
+
// INTERNAL marker only — reclassifySource() intercepts it before it can
|
|
78
|
+
// ever reach a caller as a coverageStatus.
|
|
79
|
+
'agent-tool': Object.freeze({ category: null, status: 'split', why: 'REFINED PER ENTRY: arguments (model-produced) vs results/resources (returned to the model) are different SOURCE_CATEGORIES' }),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
83
|
+
// §4.1 — agent-tool splits directionally. 8 rows.
|
|
84
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export const AGENT_TOOL_REFINEMENT = Object.freeze({
|
|
87
|
+
'py-mcp-tool': Object.freeze({ category: 'ai-model-output', status: 'partial', why: 'the tool parameter is whatever the model chose to pass; ai-model-output normally denotes a completion, so this is an approximation' }),
|
|
88
|
+
'py-mcp-server-tool': Object.freeze({ category: 'ai-model-output', status: 'partial', why: 'as py-mcp-tool' }),
|
|
89
|
+
'js-mcp-call-args': Object.freeze({ category: 'ai-model-output', status: 'partial', why: 'as py-mcp-tool' }),
|
|
90
|
+
'js-mcp-request-params': Object.freeze({ category: 'ai-model-output', status: 'partial', why: 'as py-mcp-tool' }),
|
|
91
|
+
'js-mcp-extra-args': Object.freeze({ category: 'ai-model-output', status: 'partial', why: 'as py-mcp-tool' }),
|
|
92
|
+
'js-mcp-tool-result': Object.freeze({ category: 'ai-tool-result', status: 'modeled', why: 'literally a tool result' }),
|
|
93
|
+
'py-mcp-tool-result': Object.freeze({ category: 'ai-tool-result', status: 'modeled', why: 'literally a tool result' }),
|
|
94
|
+
'js-mcp-resource-contents': Object.freeze({ category: 'ai-retrieved-document', status: 'modeled', why: 'an MCP resource IS a retrieved document' }),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
98
|
+
// §4.2 — C's I/O primitives are descriptor-generic. Within these two
|
|
99
|
+
// provenance buckets, a `cpp` entry cannot say whether it read a file, a
|
|
100
|
+
// socket, a pipe or stdin, so it is refined on top of PROVENANCE_MAP:
|
|
101
|
+
// category unchanged, status demoted to `partial`. This is a REFINEMENT of
|
|
102
|
+
// a declared value, not a fallback for a missing one (that's §4.3, a
|
|
103
|
+
// different mechanism entirely).
|
|
104
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
export const CPP_DESCRIPTOR_GENERIC_PROVENANCE = new Set(['network', 'file-read']);
|
|
107
|
+
|
|
108
|
+
const CPP_DESCRIPTOR_GENERIC_WHY = Object.freeze({
|
|
109
|
+
'network': 'LOSSY: a raw socket receive is directionally ambiguous — on a client socket this is an API response, on a listening socket it is an inbound client request, which external-api-response mis-describes',
|
|
110
|
+
'file-read': 'LOSSY: a C file descriptor / FILE* is equally a file, socket, pipe or stdin; the entry cannot say which',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
114
|
+
// §4.3 — the 82 source entries carrying NO `provenance` field at all. The
|
|
115
|
+
// category comes from the entry's own descriptive metadata (id/label/
|
|
116
|
+
// framework), never from a field its author set for classification — hence
|
|
117
|
+
// `candidate` for every row, per §6.3's decision procedure. Per §9.1, THIS
|
|
118
|
+
// table is the permanent home for these mappings (the design-phase PoC's
|
|
119
|
+
// own copy is redundant now that this module ships). Keys must equal
|
|
120
|
+
// EXACTLY the set of source entries with no declared `provenance` — see
|
|
121
|
+
// this file's own test suite for the completeness guard that enforces
|
|
122
|
+
// that, in both directions, against the live catalog.
|
|
123
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
export const NO_PROVENANCE_OVERRIDES = Object.freeze({
|
|
126
|
+
// Python / Flask
|
|
127
|
+
'py-flask-request-args': 'http-query',
|
|
128
|
+
'py-flask-request-form': 'http-body',
|
|
129
|
+
'py-flask-request-json': 'http-body',
|
|
130
|
+
'py-flask-request-values': 'http-query', // merged query+form bag
|
|
131
|
+
'py-flask-request-cookies': 'http-cookie',
|
|
132
|
+
'py-flask-request-headers': 'http-header',
|
|
133
|
+
'py-flask-request-data': 'http-body',
|
|
134
|
+
// Python / FastAPI (call form; the annotation form DOES carry provenance)
|
|
135
|
+
'py-fastapi-request-query': 'http-query',
|
|
136
|
+
'py-fastapi-request-body': 'http-body',
|
|
137
|
+
'py-fastapi-form': 'http-body',
|
|
138
|
+
// Python / Django
|
|
139
|
+
'py-django-request-GET': 'http-query',
|
|
140
|
+
'py-django-request-POST': 'http-body',
|
|
141
|
+
'py-django-request-FILES': 'http-upload',
|
|
142
|
+
'py-django-request-META': 'http-header',
|
|
143
|
+
// Python / stdlib
|
|
144
|
+
'py-os-getenv': 'env-value',
|
|
145
|
+
'py-os-environ': 'env-value',
|
|
146
|
+
'py-input': 'user-input',
|
|
147
|
+
// Java / Servlet
|
|
148
|
+
'java-request-getParameter': 'http-query',
|
|
149
|
+
'java-request-getHeader': 'http-header',
|
|
150
|
+
'java-request-getCookies': 'http-cookie',
|
|
151
|
+
'java-request-getInputStream': 'http-body',
|
|
152
|
+
'java-request-getReader': 'http-body',
|
|
153
|
+
// Java / stdlib
|
|
154
|
+
'java-system-getenv': 'env-value',
|
|
155
|
+
'java-system-getProperty': 'env-value', // JVM system properties, env-adjacent
|
|
156
|
+
// Java / Spring annotations
|
|
157
|
+
'java-spring-requestparam': 'http-query',
|
|
158
|
+
'java-spring-pathvariable': 'http-route',
|
|
159
|
+
'java-spring-requestbody': 'http-body',
|
|
160
|
+
'java-spring-requestheader': 'http-header',
|
|
161
|
+
// C# / ASP.NET Core annotations
|
|
162
|
+
'cs-aspnet-fromquery': 'http-query',
|
|
163
|
+
'cs-aspnet-frombody': 'http-body',
|
|
164
|
+
'cs-aspnet-fromform': 'http-body',
|
|
165
|
+
'cs-aspnet-fromroute': 'http-route',
|
|
166
|
+
'cs-aspnet-fromheader': 'http-header',
|
|
167
|
+
'cs-request-params': 'http-query', // merged bag
|
|
168
|
+
// Go / net/http
|
|
169
|
+
'go-r-form': 'http-body', // r.Form merges URL query + POST form
|
|
170
|
+
'go-r-postform': 'http-body',
|
|
171
|
+
'go-r-body': 'http-body',
|
|
172
|
+
'go-r-formvalue': 'http-body',
|
|
173
|
+
'go-r-postformvalue': 'http-body',
|
|
174
|
+
'go-r-uquery': 'http-query',
|
|
175
|
+
'go-r-uquery-get': 'http-query',
|
|
176
|
+
// Go / gin
|
|
177
|
+
'go-gin-query': 'http-query',
|
|
178
|
+
'go-gin-bindjson': 'http-body',
|
|
179
|
+
'go-gin-postform': 'http-body',
|
|
180
|
+
'go-gin-shouldbind': 'http-body',
|
|
181
|
+
'go-gin-shouldbindjson': 'http-body',
|
|
182
|
+
// Go / echo
|
|
183
|
+
'go-echo-param': 'http-route',
|
|
184
|
+
'go-echo-formvalue': 'http-body',
|
|
185
|
+
'go-echo-queryparam': 'http-query',
|
|
186
|
+
'go-echo-bind': 'http-body',
|
|
187
|
+
// Go / chi, fiber, buffalo, gorilla
|
|
188
|
+
'go-chi-urlparam': 'http-route',
|
|
189
|
+
'go-fiber-body': 'http-body',
|
|
190
|
+
'go-fiber-query': 'http-query',
|
|
191
|
+
'go-fiber-params': 'http-route',
|
|
192
|
+
'go-fiber-formvalue': 'http-body',
|
|
193
|
+
'go-fiber-cookies': 'http-cookie',
|
|
194
|
+
'go-fiber-bodyparser': 'http-body',
|
|
195
|
+
'go-buffalo-param': 'http-route',
|
|
196
|
+
'go-buffalo-request': 'http-body',
|
|
197
|
+
'go-gorilla-vars': 'http-route',
|
|
198
|
+
// Ruby
|
|
199
|
+
'rb-rails-params': 'http-body', // merged query+body+route bag
|
|
200
|
+
'rb-rails-cookies': 'http-cookie',
|
|
201
|
+
'rb-rails-session': 'http-cookie', // Rails sessions are cookie-backed by default
|
|
202
|
+
'rb-env': 'env-value',
|
|
203
|
+
'rb-sinatra-request-body': 'http-body',
|
|
204
|
+
'rb-sinatra-request-env': 'http-header', // the Rack env hash carries request headers
|
|
205
|
+
'rb-sinatra-request-params': 'http-query', // merged bag
|
|
206
|
+
// PHP
|
|
207
|
+
'php-request': 'http-body', // $_REQUEST merges GET+POST+COOKIE
|
|
208
|
+
'php-get': 'http-query',
|
|
209
|
+
'php-post': 'http-body',
|
|
210
|
+
'php-cookie': 'http-cookie',
|
|
211
|
+
'php-server': 'http-header',
|
|
212
|
+
'php-symfony-query': 'http-query',
|
|
213
|
+
'php-symfony-request': 'http-body',
|
|
214
|
+
'php-symfony-cookies': 'http-cookie',
|
|
215
|
+
'php-symfony-headers': 'http-header',
|
|
216
|
+
'php-symfony-files': 'http-upload',
|
|
217
|
+
'php-symfony-content': 'http-body',
|
|
218
|
+
'php-symfony-get': 'http-query', // merged bag
|
|
219
|
+
// Kotlin
|
|
220
|
+
'kt-request-param': 'http-query',
|
|
221
|
+
'kt-request-header': 'http-header',
|
|
222
|
+
'kt-ktor-parameters': 'http-query',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
226
|
+
// Externality (§9.0's decision shape requires it on every returned
|
|
227
|
+
// decision). DESIGN_REGISTRIES.md §7.5's `CATEGORY_EXTERNALITY` table is
|
|
228
|
+
// SINK-focused only — D1's binding design does not specify a source-side
|
|
229
|
+
// equivalent, and the design-phase PoC's own `reclassifySource()` never
|
|
230
|
+
// computed one at all. This module must still emit the field (§9.0), so
|
|
231
|
+
// the value below is this increment's own resolution of that gap, derived
|
|
232
|
+
// from the one shipped precedent that exists: the flagship fixture's `web`
|
|
233
|
+
// node (`fixtures/build-flagship-fixture.mjs`) — `kind: 'source'`,
|
|
234
|
+
// `subtype: 'web-app'`, `externality: 'internal'`. That fixture models the
|
|
235
|
+
// checkout/registration form collection point as INTERNAL even though the
|
|
236
|
+
// person submitting the form is an anonymous public user — establishing
|
|
237
|
+
// that a source's externality reads the SAME counterparty rule §7.5 already
|
|
238
|
+
// states for sinks: *is the counterparty on the other side of this data
|
|
239
|
+
// exchange a party outside this program?* An end user hitting your own
|
|
240
|
+
// HTTP/GraphQL/gRPC/CLI endpoint, or your own process's env/argv/stdin, is
|
|
241
|
+
// your front door, not an outside party — so the analyzed system's own code
|
|
242
|
+
// is the counterparty, giving `internal`. (This is NOT "is the collection
|
|
243
|
+
// point itself part of the analyzed system" — every catalog source entry's
|
|
244
|
+
// collection point is trivially part of the analyzed system, or there
|
|
245
|
+
// would be no call site to catalog at all, so that framing collapses to
|
|
246
|
+
// "always true" and predicts nothing; it is specifically the identity of
|
|
247
|
+
// the COUNTERPARTY, symmetric with the sink-side rule, that this table
|
|
248
|
+
// encodes.) Nor is it "is the eventual human origin of the bytes untrusted"
|
|
249
|
+
// — every catalog source entry answers yes to that by construction, which
|
|
250
|
+
// is why it's a taint source at all, so that question cannot be what
|
|
251
|
+
// externality encodes either, or every source would trivially be
|
|
252
|
+
// `external` and the field would carry no information.
|
|
253
|
+
//
|
|
254
|
+
// This table mirrors CATEGORY_EXTERNALITY's own counterparty-rule reasoning,
|
|
255
|
+
// one class at a time:
|
|
256
|
+
// - ordinary in-app collection points (HTTP shapes, CLI, env, stdin /
|
|
257
|
+
// user-input) → `internal`, matching the flagship precedent directly:
|
|
258
|
+
// the counterparty is this program's own front door, not an outside
|
|
259
|
+
// party.
|
|
260
|
+
// - genuinely third-party-origin categories (an external API's response
|
|
261
|
+
// body, a webhook payload, anything AI-model/tool/resource-sourced) →
|
|
262
|
+
// `external`, matching CATEGORY_EXTERNALITY's identical treatment of
|
|
263
|
+
// the sink-side `external-api`/`webhook`/`ai-*` categories.
|
|
264
|
+
// - store-shaped categories (a file/object-storage read, a DB read, a
|
|
265
|
+
// queue message) → `unknown`, matching CATEGORY_EXTERNALITY's identical
|
|
266
|
+
// reasoning for `database`/`object-storage`/`cache`/`queue`: the entry
|
|
267
|
+
// gives no way to tell whether the store is local or third-party
|
|
268
|
+
// managed.
|
|
269
|
+
// - `declared` (the unreached operator-declaration path, §6.5) →
|
|
270
|
+
// `unknown`, matching the sink table's own choice for that category.
|
|
271
|
+
//
|
|
272
|
+
// Evidence-grade consequence (§7.5, carried over unchanged for the source
|
|
273
|
+
// side): the accompanying evidence grade for any of the above is at best
|
|
274
|
+
// `declared` — it comes from this table, not from executed code — never
|
|
275
|
+
// `code`. Real externality resolution is FR-202's job, landing in Milestone 2.
|
|
276
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
export const SOURCE_CATEGORY_EXTERNALITY = Object.freeze({
|
|
279
|
+
'http-body': 'internal', 'http-query': 'internal', 'http-route': 'internal',
|
|
280
|
+
'http-header': 'internal', 'http-cookie': 'internal', 'http-upload': 'internal',
|
|
281
|
+
'graphql-argument': 'internal', 'grpc-field': 'internal',
|
|
282
|
+
'cli-argument': 'internal', 'env-value': 'internal', 'user-input': 'internal',
|
|
283
|
+
'queue-message': 'unknown', 'database-read': 'unknown', 'storage-read': 'unknown',
|
|
284
|
+
'external-api-response': 'external', 'webhook-payload': 'external',
|
|
285
|
+
'ai-model-output': 'external', 'ai-tool-result': 'external',
|
|
286
|
+
'ai-retrieved-document': 'external', 'ai-memory': 'external',
|
|
287
|
+
'declared': 'unknown',
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Reclassify a single `kind: 'source'` catalog entry (from CATALOG) into
|
|
292
|
+
* DataFlowGraph v1's vocabulary. Total: every entry gets a decision, none
|
|
293
|
+
* throws, none is silently dropped (§6, §9's D2 item). Node kind is always
|
|
294
|
+
* `'source'` (§7.1). `coverageStatus` is never `'manual'` (§6.5) and never
|
|
295
|
+
* the internal `'split'` marker (intercepted before it can escape).
|
|
296
|
+
*
|
|
297
|
+
* @param {object} entry a CATALOG entry with `kind === 'source'`
|
|
298
|
+
* @returns {{kind: 'source', category: string|null, coverageStatus: string, externality: string, reason: string}}
|
|
299
|
+
*/
|
|
300
|
+
export function reclassifySource(entry) {
|
|
301
|
+
if (entry.provenance) {
|
|
302
|
+
const row = PROVENANCE_MAP[entry.provenance];
|
|
303
|
+
if (!row) {
|
|
304
|
+
return {
|
|
305
|
+
kind: 'source', category: null, coverageStatus: 'unsupported',
|
|
306
|
+
externality: 'unknown', reason: `unmapped provenance ${entry.provenance}`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (entry.language === 'cpp' && CPP_DESCRIPTOR_GENERIC_PROVENANCE.has(entry.provenance)) {
|
|
310
|
+
const category = row.category;
|
|
311
|
+
return {
|
|
312
|
+
kind: 'source', category, coverageStatus: 'partial',
|
|
313
|
+
externality: SOURCE_CATEGORY_EXTERNALITY[category] ?? 'unknown',
|
|
314
|
+
reason: CPP_DESCRIPTOR_GENERIC_WHY[entry.provenance],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (row.status === 'split') {
|
|
318
|
+
const ref = AGENT_TOOL_REFINEMENT[entry.id];
|
|
319
|
+
if (!ref) {
|
|
320
|
+
return {
|
|
321
|
+
kind: 'source', category: null, coverageStatus: 'unsupported',
|
|
322
|
+
externality: 'unknown', reason: `unrefined ${entry.provenance} entry ${entry.id}`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
kind: 'source', category: ref.category, coverageStatus: ref.status,
|
|
327
|
+
externality: SOURCE_CATEGORY_EXTERNALITY[ref.category] ?? 'unknown',
|
|
328
|
+
reason: ref.why,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
kind: 'source', category: row.category, coverageStatus: row.status,
|
|
333
|
+
externality: SOURCE_CATEGORY_EXTERNALITY[row.category] ?? 'unknown',
|
|
334
|
+
reason: row.why,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const category = NO_PROVENANCE_OVERRIDES[entry.id];
|
|
338
|
+
if (!category) {
|
|
339
|
+
return {
|
|
340
|
+
kind: 'source', category: null, coverageStatus: 'unsupported',
|
|
341
|
+
externality: 'unknown', reason: `no provenance field and no override for ${entry.id}`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
// No declared classification field: inferred from the entry's own
|
|
345
|
+
// descriptive metadata. Plausible, reviewed, but not author-declared —
|
|
346
|
+
// hence `candidate`, never `modeled`/`partial` (§6.3).
|
|
347
|
+
return {
|
|
348
|
+
kind: 'source', category, coverageStatus: 'candidate',
|
|
349
|
+
externality: SOURCE_CATEGORY_EXTERNALITY[category] ?? 'unknown',
|
|
350
|
+
reason: 'inferred from entry id/label/framework; catalog.js declares no provenance for this entry',
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Re-exported for callers that want the raw source-entry slice without
|
|
355
|
+
// re-filtering CATALOG themselves (the completeness guards in this
|
|
356
|
+
// module's own test suite are the primary consumer).
|
|
357
|
+
export const SOURCE_ENTRIES = Object.freeze(CATALOG.filter((e) => e.kind === 'source'));
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
//
|
|
2
|
+
// source-seeding.js — Sub-project E, increment 2 (E2).
|
|
3
|
+
//
|
|
4
|
+
// Extracts the already-designed-and-proven source-seeding mechanism out of
|
|
5
|
+
// `DESIGN_GRAPH_BUILDER.md` §3 (binding ADR) and its throwaway
|
|
6
|
+
// proof-of-concept (`test/lineage/graph-builder-poc.test.js`'s
|
|
7
|
+
// `exprRoots`/`exprChildren`/`walkExpr`/`seedPathFor`/`planSeeds`/
|
|
8
|
+
// `seedEntryStateFactory`, `E1/1`-`E1/5`, `E1/14`) into a real, permanent,
|
|
9
|
+
// shipped module. This closes the measured "0 hops on real code" gap: the
|
|
10
|
+
// shipped `driver.js` (Sub-project E, increment 1) hardcodes `emptyState()`
|
|
11
|
+
// and produces exactly zero hops on real code with no seeding hook — see
|
|
12
|
+
// `DESIGN_GRAPH_BUILDER.md` §2.1. This module is that hook's first real
|
|
13
|
+
// consumer.
|
|
14
|
+
//
|
|
15
|
+
// This is mechanical porting of an already-reviewed design, not a redesign
|
|
16
|
+
// — see `DESIGN_GRAPH_BUILDER.md` §3 in full for the binding rules this
|
|
17
|
+
// file implements, and its own header for why the PoC (not this document)
|
|
18
|
+
// is authoritative if the two ever disagree.
|
|
19
|
+
//
|
|
20
|
+
// Reuse boundary (§12, confirmed against the source): imports ONLY
|
|
21
|
+
// `matchSource` from `../dataflow/catalog.js` and `accessPathOf` from
|
|
22
|
+
// `../dataflow/access-paths.js` — never `dataflow/engine.js`'s live taint
|
|
23
|
+
// state, never `dataflow/summaries.js`'s SummaryCache, never
|
|
24
|
+
// `dataflow/index.js`'s `runDeepAnalysis` (PRD §18.1). Everything else this
|
|
25
|
+
// module needs comes from already-shipped `src/lineage/` modules.
|
|
26
|
+
|
|
27
|
+
import { matchSource } from '../dataflow/catalog.js';
|
|
28
|
+
import { accessPathOf } from '../dataflow/access-paths.js';
|
|
29
|
+
import { emptyState, addIdentity } from './field-identity.js';
|
|
30
|
+
import { reclassifySource } from './source-registry.js';
|
|
31
|
+
import { classifyDataElementName } from './classification.js';
|
|
32
|
+
import { dataElementId } from './ids.js';
|
|
33
|
+
|
|
34
|
+
// ── §3.1: where the matched expressions come from ──────────────────────────
|
|
35
|
+
//
|
|
36
|
+
// Walk `fn.cfg.nodes` and, per node, the SAME expression roots `engine.js`'s
|
|
37
|
+
// own `step()` switch reads: `assign` → `source`, `call` → `callee` + `args`,
|
|
38
|
+
// `return` → `value`. Then recurse into every sub-expression. Deliberately
|
|
39
|
+
// NOT `fn.reads`/`fn.calls`: Sub-project D5 already measured that a call
|
|
40
|
+
// used as an assignment RHS never reaches `fn.calls[]` at all, so those
|
|
41
|
+
// side-channels are incomplete for this purpose. `fn.cfg.nodes` is a plain
|
|
42
|
+
// `Object` at runtime, not a `Map` (also D5's finding).
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The expression roots a single CFG node carries, mirroring `engine.js`'s
|
|
46
|
+
* own `step()` switch exactly. Exported: this walking primitive is not
|
|
47
|
+
* seeding-specific — E3's projection (sink enumeration, transformation
|
|
48
|
+
* attribution) needs the identical CFG-node → expression-roots traversal,
|
|
49
|
+
* and a second, independently-drifting copy is exactly the duplication
|
|
50
|
+
* this package avoids everywhere else (see `DESIGN_REGISTRIES.md` §1).
|
|
51
|
+
*/
|
|
52
|
+
export function exprRoots(node) {
|
|
53
|
+
const r = [];
|
|
54
|
+
if (node.kind === 'assign' && node.source) r.push(node.source);
|
|
55
|
+
if (node.kind === 'call') {
|
|
56
|
+
if (node.callee) r.push(node.callee);
|
|
57
|
+
for (const a of node.args ?? []) r.push(a);
|
|
58
|
+
}
|
|
59
|
+
if (node.kind === 'return' && node.value) r.push(node.value);
|
|
60
|
+
return r;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The sub-expressions of `e`, for recursive descent during the walk. */
|
|
64
|
+
function exprChildren(e) {
|
|
65
|
+
switch (e.kind) {
|
|
66
|
+
case 'member': return [e.object];
|
|
67
|
+
case 'call': return [e.callee, ...(e.args ?? [])];
|
|
68
|
+
case 'tpl': return e.parts ?? [];
|
|
69
|
+
case 'binary': case 'logical': return [e.left, e.right];
|
|
70
|
+
case 'union': return e.options ?? [];
|
|
71
|
+
case 'array': return e.elements ?? [];
|
|
72
|
+
case 'object': return (e.props ?? []).map((p) => p.value);
|
|
73
|
+
case 'assign-expr': return [e.source ?? e.value];
|
|
74
|
+
default: return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Depth-first walk over an expression tree, visiting every node once.
|
|
80
|
+
* Exported for the same reuse reason as `exprRoots` above.
|
|
81
|
+
*/
|
|
82
|
+
export function walkExpr(e, visit, parent = null) {
|
|
83
|
+
if (!e || typeof e !== 'object') return;
|
|
84
|
+
visit(e, parent);
|
|
85
|
+
for (const c of exprChildren(e)) walkExpr(c, visit, e);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── §3.2: the seed-path rule — seed the FIELD, not the container ───────────
|
|
89
|
+
//
|
|
90
|
+
// `matchSource` matches the CONTAINER (`req.body`), but the thing that has
|
|
91
|
+
// a field identity is the FIELD (`req.body.card_number`). Extend the
|
|
92
|
+
// matched expression outward through every enclosing pure-member access,
|
|
93
|
+
// then take `accessPathOf` of the outermost node. Falls back to the matched
|
|
94
|
+
// expression's own path when it is not the object of a member access
|
|
95
|
+
// (`User.create(req.body)`) — that container-level seed is the honest
|
|
96
|
+
// answer for that shape. §3.2's own text: "Classification is impossible
|
|
97
|
+
// without this rule." This must be ported EXACTLY as the PoC implements it
|
|
98
|
+
// — the single most consequential rule in the whole design.
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Extend `expr` outward through every enclosing pure-member access (using
|
|
102
|
+
* `parentOf`, a map from expression node to its immediate parent expression
|
|
103
|
+
* within the same CFG-node walk), then return `accessPathOf` of the
|
|
104
|
+
* outermost such node — the FIELD path, not the container path.
|
|
105
|
+
*/
|
|
106
|
+
function seedPathFor(expr, parentOf) {
|
|
107
|
+
let cur = expr;
|
|
108
|
+
for (;;) {
|
|
109
|
+
const p = parentOf.get(cur);
|
|
110
|
+
if (p && p.kind === 'member' && p.object === cur && typeof p.prop === 'string') { cur = p; continue; }
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
return accessPathOf(cur);
|
|
114
|
+
}
|
|
115
|
+
// task review N-1: `p.prop === 'string'` is also true for the parser's own
|
|
116
|
+
// computed-key-unknown marker `'*'` (`req.body[k]`), so this rule can seed
|
|
117
|
+
// a path like `req.body.*` or `req.body.*.name`, minting a data element
|
|
118
|
+
// literally named `*`. This is NOT a correctness bug in seeding itself —
|
|
119
|
+
// `engine.js`'s `definitePrefixBeforeWildcard` already keeps every hop
|
|
120
|
+
// path at the DEFINITE prefix (`req.body`), so the provenance DAG is
|
|
121
|
+
// unaffected (verified: `function h(req,k,db){const v=req.body[k];
|
|
122
|
+
// db.query(v);}` still produces the correct hop/node/edge shape). It DOES
|
|
123
|
+
// mean a `*`-named seed can reach `planSeeds`' own output — E3's own
|
|
124
|
+
// projection module, which mints real `DataFlowGraph v1` dataElements
|
|
125
|
+
// from seed records, must not surface a literal `*` name to a user
|
|
126
|
+
// without translating it into an honest "dynamic key" disclosure.
|
|
127
|
+
|
|
128
|
+
// ── §3.3/§3.4: planSeeds ────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Plan the seeds for a whole project. Pure: runs no field-identity analysis
|
|
132
|
+
* itself, only walks IR expressions and consults the source registry.
|
|
133
|
+
*
|
|
134
|
+
* `callGraph` must be a real object from
|
|
135
|
+
* `scanner/src/ir/callgraph.js#buildCallGraph` (`{functions, ...}`) or an
|
|
136
|
+
* equivalent hand-built fixture exposing the same shape.
|
|
137
|
+
*
|
|
138
|
+
* Returns `{seeds, unseedable}`. Each seed record carries exactly the
|
|
139
|
+
* §3.3 shape:
|
|
140
|
+
* { file, qid, nodeId, line, entryId, seedPath, canonicalName,
|
|
141
|
+
* category, coverageStatus, externality, reason, // <- reclassifySource(entry)
|
|
142
|
+
* dataElementId, dataClasses } // <- ids.js + classification.js
|
|
143
|
+
*
|
|
144
|
+
* A matched expression with NO access path (`accessPathOf` → `null`) is
|
|
145
|
+
* recorded in `unseedable[]` — never silently dropped (§3.3).
|
|
146
|
+
*/
|
|
147
|
+
export function planSeeds(callGraph, { repository } = {}) {
|
|
148
|
+
const seeds = [];
|
|
149
|
+
const unseedable = [];
|
|
150
|
+
for (const fn of callGraph.functions.values()) {
|
|
151
|
+
for (const [nid, node] of Object.entries(fn.cfg?.nodes ?? {})) {
|
|
152
|
+
const parentOf = new Map();
|
|
153
|
+
for (const root of exprRoots(node)) walkExpr(root, (e, p) => { if (p) parentOf.set(e, p); });
|
|
154
|
+
for (const root of exprRoots(node)) {
|
|
155
|
+
walkExpr(root, (e) => {
|
|
156
|
+
const entry = matchSource(e, fn.file);
|
|
157
|
+
if (!entry) return;
|
|
158
|
+
const decision = reclassifySource(entry);
|
|
159
|
+
const seedPath = seedPathFor(e, parentOf);
|
|
160
|
+
if (!seedPath) {
|
|
161
|
+
unseedable.push({
|
|
162
|
+
file: fn.file, qid: fn.qid, nodeId: nid, line: node.line ?? null,
|
|
163
|
+
entryId: entry.id, reason: 'accessPathOf returned null for the matched expression',
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const canonicalName = seedPath.slice(seedPath.lastIndexOf('.') + 1);
|
|
168
|
+
seeds.push({
|
|
169
|
+
file: fn.file, qid: fn.qid, nodeId: nid, line: node.line ?? null,
|
|
170
|
+
entryId: entry.id, seedPath, canonicalName,
|
|
171
|
+
category: decision.category, coverageStatus: decision.coverageStatus,
|
|
172
|
+
externality: decision.externality, reason: decision.reason,
|
|
173
|
+
// §3.4's minting rule. `canonicalName` alone is forbidden by
|
|
174
|
+
// PRD §10.4; the discriminator carries the system proxy
|
|
175
|
+
// (repository + file), the access path, and the category.
|
|
176
|
+
// Function scope (`qid`) is deliberately NOT in the
|
|
177
|
+
// discriminator (§3.4).
|
|
178
|
+
dataElementId: dataElementId(canonicalName, [repository, fn.file, seedPath, decision.category ?? '']),
|
|
179
|
+
dataClasses: classifyDataElementName(canonicalName).classes,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { seeds, unseedable };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── §3.5: seedEntryStateFactory — the driver.js opts.seedEntryState hook ──
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Builds the `opts.seedEntryState(fn) -> state | undefined` function
|
|
192
|
+
* `driver.js`'s `runFieldIdentityAnalysis` expects (Sub-project E,
|
|
193
|
+
* increment 1's additive hook). Groups `seeds` by function (`qid`) and
|
|
194
|
+
* returns, for a given function, a `field-identity.js` state carrying every
|
|
195
|
+
* data-element identity seeded at a matched call site inside that function
|
|
196
|
+
* — or `undefined` for a function with no seeds, so `driver.js`'s own
|
|
197
|
+
* `opts.seedEntryState(fn) || emptyState()` fallback applies unchanged.
|
|
198
|
+
*/
|
|
199
|
+
export function seedEntryStateFactory(seeds) {
|
|
200
|
+
const byQid = new Map();
|
|
201
|
+
for (const s of seeds) {
|
|
202
|
+
if (!byQid.has(s.qid)) byQid.set(s.qid, []);
|
|
203
|
+
byQid.get(s.qid).push(s);
|
|
204
|
+
}
|
|
205
|
+
return (fn) => {
|
|
206
|
+
const list = byQid.get(fn.qid);
|
|
207
|
+
if (!list) return undefined;
|
|
208
|
+
let st = emptyState();
|
|
209
|
+
for (const s of list) st = addIdentity(st, s.seedPath, s.dataElementId);
|
|
210
|
+
return st;
|
|
211
|
+
};
|
|
212
|
+
}
|