@clear-capabilities/agentic-security-scanner 0.144.0 → 0.147.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 +390 -0
- package/bin/agentic-security.js +3813 -83
- package/dist/1122.index.js +702 -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} +13 -4
- 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/2271.index.js +165 -0
- 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 +307 -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} +14 -6
- 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} +14 -6
- 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} +67 -3
- package/dist/5051.index.js +770 -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} +29 -7
- package/dist/{449.index.js → 5830.index.js} +78 -14
- 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} +26 -8
- 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} +580 -187
- 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/package.json +23 -10
- package/src/compare.js +6 -1
- package/src/dataflow/CLAUDE.md +2 -2
- package/src/dataflow/catalog.js +42 -0
- package/src/dataflow/orm-write-catalog.js +175 -0
- package/src/engine.js +580 -30
- package/src/fix/apply-fix-service.js +1 -0
- package/src/history-scan.js +22 -5
- package/src/ir/CLAUDE.md +2 -1
- 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/lsp/server.js +49 -2
- 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 +22 -1
- package/src/pipeline/assurance-mode.js +64 -1
- package/src/pipeline/finding-schema.js +8 -1
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/CLAUDE.md +140 -0
- package/src/posture/accuracy-scorecard.js +60 -0
- package/src/posture/artifact-registry.js +76 -0
- package/src/posture/auditor-walkthrough.js +192 -13
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/src/posture/compliance-policy.js +12 -2
- package/src/posture/cross-repo-memory.js +7 -2
- package/src/posture/fix-history.js +25 -2
- package/src/posture/fix-verify.js +9 -1
- package/src/posture/fleet.js +0 -0
- package/src/posture/git-history.js +13 -5
- package/src/posture/material-change.js +21 -2
- package/src/posture/mttr.js +75 -12
- package/src/posture/obligation-evidence-pack.js +202 -0
- package/src/posture/pre-incident-archaeology.js +39 -7
- package/src/posture/privacy-framework.js +14 -0
- package/src/posture/provenance/ai-authorship.js +68 -0
- package/src/posture/provenance/branch-entry.js +80 -0
- package/src/posture/provenance/cache.js +143 -0
- package/src/posture/provenance/confidence.js +36 -0
- package/src/posture/provenance/coordinator.js +786 -0
- package/src/posture/provenance/dag-walk.js +249 -0
- package/src/posture/provenance/evidence-attribution.js +59 -0
- package/src/posture/provenance/git-evidence.js +310 -0
- package/src/posture/provenance/lifecycle.js +208 -0
- package/src/posture/provenance/missing-control-resolver.js +137 -0
- package/src/posture/provenance/origin-resolver.js +342 -0
- package/src/posture/provenance/predicate-replay.js +133 -0
- package/src/posture/provenance/providers/config.js +39 -0
- package/src/posture/provenance/providers/github.js +62 -0
- package/src/posture/provenance/providers/gitlab.js +58 -0
- package/src/posture/provenance/repo-lineage.js +74 -0
- package/src/posture/provenance/sca-origin.js +139 -0
- package/src/posture/provenance/schema.js +255 -0
- package/src/posture/provenance/transitive-sca.js +147 -0
- package/src/posture/provenance/validate.js +30 -0
- package/src/posture/provenance-evidence-bundle.js +144 -0
- package/src/posture/remediation-ledger.js +337 -0
- package/src/posture/sbom-diff.js +15 -2
- package/src/posture/secret-history.js +10 -2
- package/src/posture/state-dir.js +38 -14
- package/src/posture/vuln-archaeology.js +8 -2
- package/src/pr-delta.js +25 -4
- package/src/report/index.js +197 -3
- package/src/runScan.js +34 -5
- package/src/sast/rate-limit.js +33 -3
- 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 +139 -0
- package/src/util/git-hardening.js +128 -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,251 @@
|
|
|
1
|
+
// scenario-engine.js — M5 deliverable #3a (FR-502): the clone-and-
|
|
2
|
+
// override engine. Deep-clones the base graph, applies each declared
|
|
3
|
+
// Scenario operation, then re-runs the SAME two pure aggregators
|
|
4
|
+
// graph-builder.js's own real pipeline uses (aggregateVerdicts,
|
|
5
|
+
// isSinkPermitted) over only the flows an operation actually touched —
|
|
6
|
+
// never re-running the taint/path pipeline, per this sub-project's own
|
|
7
|
+
// scoping doc.
|
|
8
|
+
//
|
|
9
|
+
// The exact recomputation this module mirrors, confirmed by direct
|
|
10
|
+
// read of graph-builder.js's own real edge/flow minting pass:
|
|
11
|
+
// flow.protectionSummary = aggregateVerdicts([
|
|
12
|
+
// edge.protection.transit.verdict,
|
|
13
|
+
// edge.protection.atRest.verdict,
|
|
14
|
+
// edge.protection.handling.verdict,
|
|
15
|
+
// ])
|
|
16
|
+
// flow.policyVerdict = isSinkPermitted(dataElement.dataClasses, sinkNode.subtype,
|
|
17
|
+
// opts.privacySinkPolicy, { environment: opts.environment, destination: sinkNode.destination?.literalValue ?? null })
|
|
18
|
+
// ? 'permitted' : 'prohibited' (or 'not_evaluated' if no policy/classes/sinkKind)
|
|
19
|
+
|
|
20
|
+
import { aggregateVerdicts } from './protection.js';
|
|
21
|
+
import { isSinkPermitted } from '../dataflow/privacy-sink-policy.js';
|
|
22
|
+
|
|
23
|
+
function _deepClone(v) { return JSON.parse(JSON.stringify(v)); }
|
|
24
|
+
|
|
25
|
+
function _byId(arr) { return new Map((arr ?? []).map((x) => [x.id, x])); }
|
|
26
|
+
|
|
27
|
+
// Aggregates protectionSummary across EVERY edge belonging to `flow`, not
|
|
28
|
+
// just one of them — a flow's `edgeIds` can in principle name more than
|
|
29
|
+
// one edge (graph-builder.js's own real flows are always single-edge
|
|
30
|
+
// today, but the schema and this module both accept a multi-edge array
|
|
31
|
+
// with no restriction), and aggregateVerdicts's own documented purpose is
|
|
32
|
+
// exactly this cross-branch/cross-edge reduction (PRD §8.4). Picking a
|
|
33
|
+
// single arbitrary edge here would silently go stale the moment a
|
|
34
|
+
// scenario is built against a genuinely multi-hop flow.
|
|
35
|
+
function _recomputeProtectionSummaryForFlow(flow, graph) {
|
|
36
|
+
const edges = graph.edges.filter((e) => flow.edgeIds.includes(e.id));
|
|
37
|
+
const verdicts = edges.flatMap((e) => [
|
|
38
|
+
e.protection.transit.verdict,
|
|
39
|
+
e.protection.atRest.verdict,
|
|
40
|
+
e.protection.handling.verdict,
|
|
41
|
+
]);
|
|
42
|
+
return aggregateVerdicts(verdicts);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _recomputePolicyVerdict(flow, graph, opts) {
|
|
46
|
+
if (opts.privacySinkPolicy == null) return null; // signal: leave flow.policyVerdict untouched
|
|
47
|
+
const de = graph.dataElements.find((d) => flow.dataElementIds.includes(d.id));
|
|
48
|
+
const sinkNode = graph.nodes.find((n) => n.id === flow.sink);
|
|
49
|
+
const classes = de?.dataClasses ?? [];
|
|
50
|
+
const sinkKind = sinkNode?.subtype ?? null;
|
|
51
|
+
if (!classes.length || !sinkKind) return 'not_evaluated';
|
|
52
|
+
const ctx = {
|
|
53
|
+
environment: opts.environment ?? process.env.AGENTIC_SECURITY_ENVIRONMENT ?? null,
|
|
54
|
+
destination: sinkNode?.destination?.literalValue ?? null,
|
|
55
|
+
};
|
|
56
|
+
return isSinkPermitted(classes, sinkKind, opts.privacySinkPolicy, ctx) ? 'permitted' : 'prohibited';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Every flow using this edge (by edgeIds membership) gets protectionSummary
|
|
60
|
+
// recomputed; policyVerdict only for flows whose sink node's destination
|
|
61
|
+
// or the flow's own dataClasses could plausibly have changed — but since
|
|
62
|
+
// applyScenario always calls this after ANY node/edge touch to be safe
|
|
63
|
+
// (recomputation is cheap and idempotent), scope is simply "every flow
|
|
64
|
+
// touching this edge or this node".
|
|
65
|
+
function _touchedFlows(graph, { edgeId, nodeId }) {
|
|
66
|
+
return graph.flows.filter((f) =>
|
|
67
|
+
(edgeId && f.edgeIds.includes(edgeId)) ||
|
|
68
|
+
(nodeId && (f.source === nodeId || f.sink === nodeId)));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _recomputeTouchedFlows(graph, touch, opts) {
|
|
72
|
+
for (const flow of _touchedFlows(graph, touch)) {
|
|
73
|
+
flow.protectionSummary = _recomputeProtectionSummaryForFlow(flow, graph);
|
|
74
|
+
const newPolicyVerdict = _recomputePolicyVerdict(flow, graph, opts);
|
|
75
|
+
if (newPolicyVerdict !== null) flow.policyVerdict = newPolicyVerdict;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _applyRequireTransitProtection(graph, op) {
|
|
80
|
+
const edge = _byId(graph.edges).get(op.targetEdgeId);
|
|
81
|
+
if (!edge) return { ok: false, reason: `targetEdgeId "${op.targetEdgeId}" not found in graph.edges` };
|
|
82
|
+
edge.protection.transit = { verdict: 'protected', evidenceGrade: 'assumed' };
|
|
83
|
+
_recomputeTouchedFlows(graph, { edgeId: edge.id }, op._opts);
|
|
84
|
+
return { ok: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _applyHandling(graph, op) {
|
|
88
|
+
const edge = _byId(graph.edges).get(op.targetEdgeId);
|
|
89
|
+
if (!edge) return { ok: false, reason: `targetEdgeId "${op.targetEdgeId}" not found in graph.edges` };
|
|
90
|
+
// Mirrors graph-builder.js's own gate (~line 757): 'encrypted' handling
|
|
91
|
+
// only earns the atRest 'protected' verdict when the edge's sink node
|
|
92
|
+
// is store-kind (database/file/object-storage/cache/client-storage/
|
|
93
|
+
// backup/export). For any other sink kind (e.g. 'external-api',
|
|
94
|
+
// 'queue') "at rest" isn't even a meaningful concept — falls through to
|
|
95
|
+
// the same 'handling' dimension the non-encrypted case already sets.
|
|
96
|
+
const sinkNode = graph.nodes.find((n) => n.id === edge.to);
|
|
97
|
+
if (op.handling === 'encrypted' && sinkNode?.kind === 'store') {
|
|
98
|
+
edge.protection.atRest = { verdict: 'protected', evidenceGrade: 'assumed' };
|
|
99
|
+
} else {
|
|
100
|
+
edge.protection.handling = { verdict: 'protected', evidenceGrade: 'assumed' };
|
|
101
|
+
}
|
|
102
|
+
_recomputeTouchedFlows(graph, { edgeId: edge.id }, op._opts);
|
|
103
|
+
return { ok: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function _applyRemoveEntity(graph, op) {
|
|
107
|
+
const node = _byId(graph.nodes).get(op.targetNodeId);
|
|
108
|
+
if (!node) return { ok: false, reason: `targetNodeId "${op.targetNodeId}" not found in graph.nodes` };
|
|
109
|
+
const removedEdgeIds = new Set(graph.edges.filter((e) => e.from === node.id || e.to === node.id).map((e) => e.id));
|
|
110
|
+
graph.edges = graph.edges.filter((e) => !removedEdgeIds.has(e.id));
|
|
111
|
+
graph.flows = graph.flows.filter((f) => f.source !== node.id && f.sink !== node.id
|
|
112
|
+
&& !f.edgeIds.some((id) => removedEdgeIds.has(id)));
|
|
113
|
+
graph.nodes = graph.nodes.filter((n) => n.id !== node.id);
|
|
114
|
+
// Cascade-prune graph.recipientProfiles[] too — a RecipientProfile whose
|
|
115
|
+
// contributingGraphIds referenced ONLY this now-removed node must not
|
|
116
|
+
// survive dangling (a downstream `dataflow export --format recipients`
|
|
117
|
+
// consumer filters by exactly contributingGraphIds against surviving
|
|
118
|
+
// node ids); a profile with OTHER surviving contributing ids just loses
|
|
119
|
+
// this one id, never the whole record. Guarded on the array existing —
|
|
120
|
+
// the base graph shape used throughout this sub-project's own tests
|
|
121
|
+
// doesn't always include it. A profile with NO contributingGraphIds at
|
|
122
|
+
// all (null/undefined — real graph-builder.js output always mints a
|
|
123
|
+
// non-empty array, so this is defensive against a hand-built/external
|
|
124
|
+
// graph only) is left untouched rather than normalized to `[]` and
|
|
125
|
+
// dropped, since a missing array is not evidence the profile referenced
|
|
126
|
+
// this node.
|
|
127
|
+
// NOTE (final-review re-review N2, disclosed not fixed): this cascade
|
|
128
|
+
// is real graph mutation, but scenario-diff.js's WATCHED_SCENARIO_FIELDS
|
|
129
|
+
// has no `recipientProfile` kind, so diffScenarioGraph's own delta
|
|
130
|
+
// report stays silent about a recipient record that disappeared or lost
|
|
131
|
+
// a contributing id here — a future increment's fuller fix, not scope
|
|
132
|
+
// creep for this pass.
|
|
133
|
+
if (graph.recipientProfiles?.length) {
|
|
134
|
+
graph.recipientProfiles = graph.recipientProfiles
|
|
135
|
+
.map((rp) => (rp.contributingGraphIds == null ? rp
|
|
136
|
+
: { ...rp, contributingGraphIds: rp.contributingGraphIds.filter((id) => id !== node.id) }))
|
|
137
|
+
.filter((rp) => rp.contributingGraphIds == null || rp.contributingGraphIds.length > 0);
|
|
138
|
+
}
|
|
139
|
+
return { ok: true };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Scoped, at the applier level, to exactly `node.destination` — this
|
|
143
|
+
// sub-project's own scoping doc maps "replace a provider or move region"
|
|
144
|
+
// onto node.destination, the graph-level equivalent of a recipient's
|
|
145
|
+
// technical endpoint, and nothing else. scenario.js's own
|
|
146
|
+
// SCENARIO_OPERATION_REQUIRED_FIELDS only requires `field` be a non-empty
|
|
147
|
+
// string, never restricts its value — a real applied write to any other
|
|
148
|
+
// top-level node field (e.g. 'subtype') would land on the node but be
|
|
149
|
+
// completely invisible to scenario-diff.js's WATCHED_SCENARIO_FIELDS.node
|
|
150
|
+
// (which only watches 'destination'/'storeDetail'), since that list can
|
|
151
|
+
// only stay exhaustive if this applier never writes outside it. Rejecting
|
|
152
|
+
// any other field here — never widening the watch list — is what keeps
|
|
153
|
+
// that list exhaustive by construction. Mirrors the existing "skip a
|
|
154
|
+
// target id it can't find" pattern: reported in skippedOperations, never
|
|
155
|
+
// thrown.
|
|
156
|
+
function _applyReplaceRecipientFact(graph, op) {
|
|
157
|
+
const node = _byId(graph.nodes).get(op.targetNodeId);
|
|
158
|
+
if (!node) return { ok: false, reason: `targetNodeId "${op.targetNodeId}" not found in graph.nodes` };
|
|
159
|
+
if (op.field !== 'destination') {
|
|
160
|
+
return { ok: false, reason: `replace_recipient_fact only supports field "destination" (got "${op.field}") — this operation is scoped to a node's recipient/destination fact only` };
|
|
161
|
+
}
|
|
162
|
+
node[op.field] = _deepClone(op.value);
|
|
163
|
+
// The destination just changed — any edge.protection.transit verdict INTO
|
|
164
|
+
// this node was derived from the OLD destination (transit-protection.js's
|
|
165
|
+
// resolveTransitProtectionForSite is keyed on the sink's own destination)
|
|
166
|
+
// and is now stale. Never let a stale verdict survive with a real (non-
|
|
167
|
+
// 'assumed') evidence grade — reset to the honest "we don't know anymore"
|
|
168
|
+
// state rather than re-derive (re-deriving needs transitEvidenceByFile,
|
|
169
|
+
// IR-level data unavailable post-build) or silently keep the old verdict.
|
|
170
|
+
// NOTE (final-review re-review N1, disclosed not fixed): this demotion is
|
|
171
|
+
// per-EDGE and is always honest at that level (and always visible in
|
|
172
|
+
// diffScenarioGraph's own delta, since edge.protection.transit is a
|
|
173
|
+
// WATCHED_SCENARIO_FIELDS.edge entry) — but flow.protectionSummary
|
|
174
|
+
// aggregates transit/atRest/handling via aggregateVerdicts, whose
|
|
175
|
+
// precedence ranks not_assessed LAST, so a scenario that ALSO sets this
|
|
176
|
+
// edge's own handling dimension to 'protected' (via apply_handling) in
|
|
177
|
+
// the SAME operations list can mask the demotion at the flow-summary
|
|
178
|
+
// level, even though the masking verdict itself is honestly graded
|
|
179
|
+
// 'assumed'. graph-builder.js's own cross-dimension aggregation is
|
|
180
|
+
// documented safe only because transit/atRest are mutually exclusive by
|
|
181
|
+
// construction and handling is "never written" by any real analyzer —
|
|
182
|
+
// this module (scenario-engine.js) is the one exception to that last
|
|
183
|
+
// clause, which is exactly what makes the masking reachable here.
|
|
184
|
+
for (const edge of graph.edges) {
|
|
185
|
+
if (edge.to === node.id) {
|
|
186
|
+
edge.protection.transit = { verdict: 'not_assessed', evidenceGrade: 'none' };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
_recomputeTouchedFlows(graph, { nodeId: node.id }, op._opts);
|
|
190
|
+
return { ok: true };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function _applyChangeStorageFact(graph, op) {
|
|
194
|
+
const node = _byId(graph.nodes).get(op.targetNodeId);
|
|
195
|
+
if (!node) return { ok: false, reason: `targetNodeId "${op.targetNodeId}" not found in graph.nodes` };
|
|
196
|
+
node.storeDetail = { ...(node.storeDetail ?? {}), [op.field]: _deepClone(op.value) };
|
|
197
|
+
return { ok: true };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function _applyChangeGovernanceFact(graph, op) {
|
|
201
|
+
const flow = _byId(graph.flows).get(op.targetFlowId);
|
|
202
|
+
if (!flow) return { ok: false, reason: `targetFlowId "${op.targetFlowId}" not found in graph.flows` };
|
|
203
|
+
flow.governanceRefs = { ...(flow.governanceRefs ?? {}), [op.field]: _deepClone(op.value) };
|
|
204
|
+
return { ok: true };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const _APPLIERS = {
|
|
208
|
+
require_transit_protection: _applyRequireTransitProtection,
|
|
209
|
+
apply_handling: _applyHandling,
|
|
210
|
+
remove_entity: _applyRemoveEntity,
|
|
211
|
+
replace_recipient_fact: _applyReplaceRecipientFact,
|
|
212
|
+
change_storage_fact: _applyChangeStorageFact,
|
|
213
|
+
change_governance_fact: _applyChangeGovernanceFact,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Apply `scenario.operations` to a deep clone of `baseGraph`. Never
|
|
218
|
+
* mutates `baseGraph`. An operation whose target id does not exist in
|
|
219
|
+
* the graph is skipped (reported in `skippedOperations`), never thrown —
|
|
220
|
+
* a Scenario written against an older snapshot must degrade honestly.
|
|
221
|
+
* `opts.privacySinkPolicy`/`opts.environment` mirror graph-builder.js's
|
|
222
|
+
* own opts; omitting privacySinkPolicy means policyVerdict
|
|
223
|
+
* recomputation is skipped entirely (base value kept on every touched
|
|
224
|
+
* flow) rather than guessed.
|
|
225
|
+
*
|
|
226
|
+
* Scenario-shape validation (`validateScenario`, `scenario.js`) is the
|
|
227
|
+
* CALLER's responsibility, not this module's — this function does no
|
|
228
|
+
* upfront shape checking beyond each operation's own per-target `_byId`
|
|
229
|
+
* lookup. An unrecognized `op.kind` is skipped via the `_APPLIERS` map
|
|
230
|
+
* lookup below rather than validated against `SCENARIO_OPERATION_KINDS`
|
|
231
|
+
* ahead of time. This mirrors `recipient-profile.js`'s own precedent,
|
|
232
|
+
* where `validateRecipientProfile` is never called internally by any
|
|
233
|
+
* producer either — the CLI layer (Task 4) calls `validateScenario`
|
|
234
|
+
* before `applyScenario`, by design.
|
|
235
|
+
*/
|
|
236
|
+
export function applyScenario(baseGraph, scenario, opts = {}) {
|
|
237
|
+
const graph = _deepClone(baseGraph);
|
|
238
|
+
const appliedOperations = [];
|
|
239
|
+
const skippedOperations = [];
|
|
240
|
+
for (const op of scenario.operations ?? []) {
|
|
241
|
+
const applier = _APPLIERS[op.kind];
|
|
242
|
+
if (!applier) {
|
|
243
|
+
skippedOperations.push({ operation: op, reason: `unrecognized operation kind "${op.kind}"` });
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const result = applier(graph, { ...op, _opts: opts });
|
|
247
|
+
if (result.ok) appliedOperations.push({ operation: op });
|
|
248
|
+
else skippedOperations.push({ operation: op, reason: result.reason });
|
|
249
|
+
}
|
|
250
|
+
return { graph, appliedOperations, skippedOperations };
|
|
251
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// scenario.js — M5 deliverable #3a (FR-502 §10.10, DFG-0xx): the
|
|
2
|
+
// Scenario extension contract — a hypothetical set of graph overrides,
|
|
3
|
+
// NOT a DataFlowGraph v1 entity, mirrors recipient-profile.js's own
|
|
4
|
+
// contract shape exactly (structural-only {valid, errors} validator,
|
|
5
|
+
// zero graph access, per-field evidence typing for the fields a
|
|
6
|
+
// Scenario actually overrides).
|
|
7
|
+
//
|
|
8
|
+
// See docs/superpowers/plans/2026-09-02-data-flow-explorer-m5-whatif-simulator-scoping.md
|
|
9
|
+
// for the full design reasoning, including why EVIDENCE_GRADES gained
|
|
10
|
+
// 'assumed' instead of reusing 'declared', and why this module's own
|
|
11
|
+
// operations catalog excludes synthetic node/edge insertion (deferred).
|
|
12
|
+
|
|
13
|
+
export const SCENARIO_VERSION = '1.0.0';
|
|
14
|
+
|
|
15
|
+
// The 6 in-scope hypothetical-change kinds (FR-502's own 7, minus the
|
|
16
|
+
// deferred synthetic-insertion category). Each operation names its
|
|
17
|
+
// target canonical id(s) plus the override value(s); scenario-engine.js
|
|
18
|
+
// is the only consumer that interprets `kind`.
|
|
19
|
+
export const SCENARIO_OPERATION_KINDS = Object.freeze([
|
|
20
|
+
'require_transit_protection',
|
|
21
|
+
'apply_handling',
|
|
22
|
+
'remove_entity',
|
|
23
|
+
'replace_recipient_fact',
|
|
24
|
+
'change_storage_fact',
|
|
25
|
+
'change_governance_fact',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
// Per-operation-kind required fields, beyond the universal `kind`. Kept
|
|
29
|
+
// as data (not inline in validateScenario) for the same reason
|
|
30
|
+
// SCENARIO_OPERATION_KINDS above is — a single source of truth a table
|
|
31
|
+
// can be checked against — though scenario-engine.js does not currently
|
|
32
|
+
// import this table itself; it only mentions it in a prose comment on
|
|
33
|
+
// its own _applyReplaceRecipientFact.
|
|
34
|
+
export const SCENARIO_OPERATION_REQUIRED_FIELDS = Object.freeze({
|
|
35
|
+
require_transit_protection: ['targetEdgeId'],
|
|
36
|
+
apply_handling: ['targetEdgeId', 'handling'],
|
|
37
|
+
remove_entity: ['targetNodeId'],
|
|
38
|
+
replace_recipient_fact: ['targetNodeId', 'field', 'value'],
|
|
39
|
+
change_storage_fact: ['targetNodeId', 'field', 'value'],
|
|
40
|
+
change_governance_fact: ['targetFlowId', 'field', 'value'],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function _isNonEmptyString(v) { return typeof v === 'string' && v.length > 0; }
|
|
44
|
+
function _isPlainObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }
|
|
45
|
+
function _isStringArray(v) { return Array.isArray(v) && v.every((x) => typeof x === 'string'); }
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Structural validation only — mirrors validateRecipientProfile's own
|
|
49
|
+
* {valid, errors} shape and "never throws" contract. Does not check
|
|
50
|
+
* that targetEdgeId/targetNodeId/targetFlowId actually exist in any
|
|
51
|
+
* real graph — that is scenario-engine.js's job at apply time, since
|
|
52
|
+
* this module has zero graph access by design.
|
|
53
|
+
*/
|
|
54
|
+
export function validateScenario(record) {
|
|
55
|
+
const errors = [];
|
|
56
|
+
const err = (p, message) => errors.push({ path: p, message });
|
|
57
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
58
|
+
err('$', 'Scenario record must be an object');
|
|
59
|
+
return { valid: false, errors };
|
|
60
|
+
}
|
|
61
|
+
if (!_isNonEmptyString(record.id) || !record.id.startsWith('scenario:')) {
|
|
62
|
+
err('$.id', 'id is required and must start with "scenario:"');
|
|
63
|
+
}
|
|
64
|
+
if (!_isNonEmptyString(record.version)) err('$.version', 'version is required');
|
|
65
|
+
if (!_isNonEmptyString(record.baseGraphId)) err('$.baseGraphId', 'baseGraphId is required');
|
|
66
|
+
if (!_isNonEmptyString(record.baseGraphDigest)) err('$.baseGraphDigest', 'baseGraphDigest is required');
|
|
67
|
+
if (!_isNonEmptyString(record.author)) err('$.author', 'author is required');
|
|
68
|
+
if (!_isNonEmptyString(record.createdAt)) err('$.createdAt', 'createdAt is required');
|
|
69
|
+
if (record.expiration !== null && record.expiration !== undefined && !_isNonEmptyString(record.expiration)) {
|
|
70
|
+
err('$.expiration', 'expiration must be a string or null');
|
|
71
|
+
}
|
|
72
|
+
if (!_isStringArray(record.assumptions ?? [])) err('$.assumptions', 'assumptions must be an array of strings');
|
|
73
|
+
if (!_isStringArray(record.verificationRequirements ?? [])) {
|
|
74
|
+
err('$.verificationRequirements', 'verificationRequirements must be an array of strings');
|
|
75
|
+
}
|
|
76
|
+
if (!Array.isArray(record.operations) || record.operations.length === 0) {
|
|
77
|
+
err('$.operations', 'operations is required and must be a non-empty array');
|
|
78
|
+
} else {
|
|
79
|
+
record.operations.forEach((op, i) => {
|
|
80
|
+
const p = `$.operations[${i}]`;
|
|
81
|
+
if (!_isPlainObject(op)) { err(p, 'each operation must be an object'); return; }
|
|
82
|
+
if (!SCENARIO_OPERATION_KINDS.includes(op.kind)) {
|
|
83
|
+
err(`${p}.kind`, `unrecognized operation kind "${op.kind}" — must be one of ${SCENARIO_OPERATION_KINDS.join('|')}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (const field of SCENARIO_OPERATION_REQUIRED_FIELDS[op.kind]) {
|
|
87
|
+
if (op[field] === undefined || op[field] === null || op[field] === '') {
|
|
88
|
+
err(`${p}.${field}`, `operation of kind "${op.kind}" requires "${field}"`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// simulatedDelta is populated by scenario-engine.js after apply, never
|
|
94
|
+
// by a caller constructing the pre-apply record — null is the only
|
|
95
|
+
// valid pre-apply value, an object (scenario-diff.js's own shape) the
|
|
96
|
+
// only valid post-apply value.
|
|
97
|
+
if (record.simulatedDelta !== null && record.simulatedDelta !== undefined && !_isPlainObject(record.simulatedDelta)) {
|
|
98
|
+
err('$.simulatedDelta', 'simulatedDelta must be null (before apply) or an object (after apply)');
|
|
99
|
+
}
|
|
100
|
+
return { valid: errors.length === 0, errors };
|
|
101
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DataFlowGraph v1 — canonical envelope and enum contract (Data Flow
|
|
3
|
+
// Explorer PRD section 10). This module carries only pure constants and
|
|
4
|
+
// the empty-envelope constructor; no analysis logic lives here. A change
|
|
5
|
+
// to any array below is a schema-version-bumping change.
|
|
6
|
+
//
|
|
7
|
+
// Isolated from scanner/src/dataflow/ on purpose (PRD section 18.1): this
|
|
8
|
+
// package may read pure, stateless exports from dataflow/privacy-taxonomy.js
|
|
9
|
+
// but must never import scanner/src/dataflow/engine.js or touch its taint
|
|
10
|
+
// state.
|
|
11
|
+
|
|
12
|
+
export const SCHEMA_VERSION = '1.0.0';
|
|
13
|
+
|
|
14
|
+
export const NODE_KINDS = Object.freeze([
|
|
15
|
+
'source', 'process', 'transform', 'api', 'store', 'queue',
|
|
16
|
+
'log', 'sink', 'external', 'boundary', 'unresolved',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export const MAPPING_TYPES = Object.freeze([
|
|
20
|
+
'identity', 'rename', 'projection', 'serialization', 'deserialization',
|
|
21
|
+
'transformation', 'aggregation', 'join', 'filter', 'sort', 'conditional', 'unknown',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export const TRANSFORM_KINDS = Object.freeze([
|
|
25
|
+
'mask', 'redact', 'tokenize', 'hash', 'encrypt', 'decrypt', 'encode',
|
|
26
|
+
'decode', 'aggregate', 'truncate', 'normalize', 'custom', 'unknown',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const REVERSIBILITY_VALUES = Object.freeze(['reversible', 'irreversible', 'unknown']);
|
|
30
|
+
|
|
31
|
+
// FR-403's single-path handling TAXONOMY (Milestone 2, Sub-project D,
|
|
32
|
+
// increment 1 — DESIGN_HANDLING_ANALYZER.md). Lives on `flow.handling`, a
|
|
33
|
+
// STRING enum — deliberately NOT the same thing as `protection.js`'s
|
|
34
|
+
// `PROTECTION_DIMENSIONS`' own `handling` dimension (an per-EDGE
|
|
35
|
+
// `{verdict, evidenceGrade}` object scored from `PROTECTION_VERDICTS`).
|
|
36
|
+
// The two share a name because this taxonomy IS what a later Milestone 2
|
|
37
|
+
// analyzer will read to populate that verdict (transform-catalog.js's own
|
|
38
|
+
// header: "Recognizing that a `mask` happened is this module's job.
|
|
39
|
+
// Deciding whether that `mask` earns 'protected' is Milestone 2's FR-401-
|
|
40
|
+
// 405 analyzers, reading this module's output.") — but the two fields are
|
|
41
|
+
// never the same value, never interchangeable, and this increment sets
|
|
42
|
+
// only `flow.handling`, never `edge.protection.handling`.
|
|
43
|
+
export const HANDLING_VALUES = Object.freeze([
|
|
44
|
+
'raw', 'masked', 'redacted', 'hashed', 'tokenized', 'encrypted', 'aggregated', 'unknown',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export const EXTERNALITY_VALUES = Object.freeze(['internal', 'external', 'unknown']);
|
|
48
|
+
|
|
49
|
+
export const COVERAGE_STATUS_VALUES = Object.freeze(['modeled', 'partial', 'candidate', 'unsupported', 'manual']);
|
|
50
|
+
|
|
51
|
+
// Milestone 5, language coverage-tier disclosure: a product-level tier for
|
|
52
|
+
// an entire LANGUAGE (per docs/METRICS.md's own curated recall measurements),
|
|
53
|
+
// deliberately NOT a reuse of COVERAGE_STATUS_VALUES above — that enum
|
|
54
|
+
// answers a different question (is this one NODE's own classification
|
|
55
|
+
// confident), not "does this language, as a whole, clear the PRD's own
|
|
56
|
+
// field-to-sink recall bar." 'full' is real and reachable (a future
|
|
57
|
+
// docs/METRICS.md update crossing 85% needs no code change here to report
|
|
58
|
+
// it) even though no language currently resolves to it.
|
|
59
|
+
export const LANGUAGE_COVERAGE_TIER_VALUES = Object.freeze(['full', 'partial', 'pattern-only', 'unknown']);
|
|
60
|
+
|
|
61
|
+
export const DESTINATION_RESOLUTION_VALUES = Object.freeze([
|
|
62
|
+
'literal', 'resolved_from_constant', 'resolved_from_config', 'resolved_from_schema',
|
|
63
|
+
'declared_service', 'runtime_corroborated', 'dynamic', 'unknown',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
// Milestone 2, Sub-project F, increment 1 (FR-304): which mechanism
|
|
67
|
+
// discovered this edge. Deliberately value-aligned with (but a
|
|
68
|
+
// distinct field from) EVIDENCE_TYPES — 'code'/'schema'/'manual'/
|
|
69
|
+
// 'runtime' are FR-304's own four categories, reusing EVIDENCE_TYPES's
|
|
70
|
+
// existing spellings rather than inventing a second vocabulary for the
|
|
71
|
+
// same four concepts. Only 'code' has a real producer today
|
|
72
|
+
// (graph-builder.js sets it unconditionally); 'schema'/'manual'/
|
|
73
|
+
// 'runtime' are reserved for Sub-project F2/F3, not yet implemented —
|
|
74
|
+
// see scanner/src/lineage/CLAUDE.md.
|
|
75
|
+
export const EDGE_PROVENANCE_VALUES = Object.freeze(['code', 'schema', 'manual', 'runtime']);
|
|
76
|
+
|
|
77
|
+
// Milestone 2, Sub-project E, increment 2 (`node.storeDetail.operation`,
|
|
78
|
+
// DESIGN_STORE_DETAIL.md). `save` deliberately maps to `'upsert'`, never
|
|
79
|
+
// `'create'` — see graph-builder.js's own method-name mapping comment for
|
|
80
|
+
// the full reasoning (Mongoose's `.save()` is an INSERT on a new document
|
|
81
|
+
// but an UPDATE on one loaded from the database, genuinely undecidable
|
|
82
|
+
// statically from the call site alone; `'upsert'` is the honest umbrella,
|
|
83
|
+
// not a guess at which one).
|
|
84
|
+
export const STORE_OPERATION_VALUES = Object.freeze(['create', 'read', 'update', 'delete', 'upsert', 'unknown']);
|
|
85
|
+
|
|
86
|
+
// Milestone 2, Sub-project E, increment 3 (`node.queueDetail.operation`,
|
|
87
|
+
// DESIGN_QUEUE_DETAIL.md). Only two values, deliberately — both real
|
|
88
|
+
// PRIVACY_SINK_CATALOG queue entries (`sendMessage`, `publish`) are
|
|
89
|
+
// unambiguously a WRITE/publish operation, unlike the database case's real
|
|
90
|
+
// `create`/`update`/`upsert` spread; there is no `read`/`delete`/`create`
|
|
91
|
+
// ambiguity to disclose here. Every queue site this increment recognizes
|
|
92
|
+
// gets `operation: 'publish'` — never `'unknown'` in practice today (no
|
|
93
|
+
// catalog entry produces anything else), but the value exists so
|
|
94
|
+
// `validate.js`'s structural check has a real enum to check against, the
|
|
95
|
+
// same "define the vocabulary even if only one value is reachable today"
|
|
96
|
+
// precedent `HANDLING_VALUES`/`STORE_OPERATION_VALUES` both already
|
|
97
|
+
// established.
|
|
98
|
+
export const QUEUE_OPERATION_VALUES = Object.freeze(['publish', 'unknown']);
|
|
99
|
+
|
|
100
|
+
export const POLICY_STATES = Object.freeze([
|
|
101
|
+
'prohibited', 'permitted', 'conditionally_permitted', 'manual_review_required', 'not_evaluated',
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
export const EVIDENCE_TYPES = Object.freeze([
|
|
105
|
+
'code', 'ir', 'configuration', 'iac', 'schema', 'service_declaration', 'policy', 'manual', 'runtime',
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
export const FLOW_SUMMARY_VALUES = Object.freeze(['protected', 'unprotected', 'mixed', 'unknown', 'not_assessed']);
|
|
109
|
+
|
|
110
|
+
// Not named in the PRD's own envelope example (section 10.2) but required
|
|
111
|
+
// by Appendix D.1's "no special-casing by name" rule: the UI needs a
|
|
112
|
+
// GENERIC signal for the "Illustrative demo data" ribbon, not a check
|
|
113
|
+
// against a specific fixture filename or node id.
|
|
114
|
+
export const GRAPH_SCOPE_SOURCES = Object.freeze(['scan', 'fixture']);
|
|
115
|
+
|
|
116
|
+
// PRD section 11 (FR-101) source categories and section 12 (FR-201) sink
|
|
117
|
+
// categories — the fixed vocabulary a node's `subtype`/an inventory row's
|
|
118
|
+
// category field draws from. This is the TAXONOMY only; the pattern-
|
|
119
|
+
// matching registries that recognize one of these categories in real
|
|
120
|
+
// source code (source-registry.js / sink-registry.js, PRD DFG-003) are
|
|
121
|
+
// Milestone 1, not this module.
|
|
122
|
+
export const SOURCE_CATEGORIES = Object.freeze([
|
|
123
|
+
'http-body', 'http-query', 'http-route', 'http-header', 'http-cookie', 'http-upload',
|
|
124
|
+
'graphql-argument', 'grpc-field', 'cli-argument', 'env-value', 'queue-message',
|
|
125
|
+
'database-read', 'storage-read', 'user-input', 'external-api-response',
|
|
126
|
+
'webhook-payload', 'ai-model-output', 'ai-tool-result', 'ai-retrieved-document',
|
|
127
|
+
'ai-memory', 'declared',
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
export const SINK_CATEGORIES = Object.freeze([
|
|
131
|
+
'log', 'stdout', 'http-response', 'client-storage', 'database', 'file', 'object-storage',
|
|
132
|
+
'cache', 'queue', 'analytics', 'monitoring', 'email', 'sms', 'push-notification', 'collaboration',
|
|
133
|
+
'external-api', 'webhook', 'ai-model-provider', 'ai-local-model', 'ai-agent', 'ai-tool',
|
|
134
|
+
'ai-vector-store', 'ai-memory', 'ai-training', 'ai-evaluation', 'ai-telemetry',
|
|
135
|
+
'backup', 'export', 'declared',
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A fresh DataFlowGraph v1 envelope with every required top-level key
|
|
140
|
+
* present. Callers overlay real content; every array starts empty and
|
|
141
|
+
* every nested object starts as `{}` except `scope.source`, which
|
|
142
|
+
* defaults to `'scan'` (a fixture builder must set it to `'fixture'`
|
|
143
|
+
* explicitly — the safe default assumes real analyzer output).
|
|
144
|
+
*/
|
|
145
|
+
export function emptyGraphEnvelope(overrides = {}) {
|
|
146
|
+
const { graphId, generatedAt, scope, ...rest } = overrides;
|
|
147
|
+
return {
|
|
148
|
+
schemaVersion: SCHEMA_VERSION,
|
|
149
|
+
graphId: graphId || null,
|
|
150
|
+
generatedAt: generatedAt || new Date().toISOString(),
|
|
151
|
+
scope: { source: 'scan', ...(scope || {}) },
|
|
152
|
+
scanHealth: {},
|
|
153
|
+
taxonomy: {},
|
|
154
|
+
nodes: [],
|
|
155
|
+
edges: [],
|
|
156
|
+
dataElements: [],
|
|
157
|
+
transformations: [],
|
|
158
|
+
flows: [],
|
|
159
|
+
controls: [],
|
|
160
|
+
policies: [],
|
|
161
|
+
evidence: [],
|
|
162
|
+
coverage: {},
|
|
163
|
+
limitations: [],
|
|
164
|
+
extensions: {},
|
|
165
|
+
...rest,
|
|
166
|
+
};
|
|
167
|
+
}
|